From 8129b174f1fe8a7b2c2d02b9ad0c6ef64caf13c6 Mon Sep 17 00:00:00 2001 From: Mario Rodriguez Lopez <101330800+MarioRgzLpz@users.noreply.github.com> Date: Tue, 20 Aug 2024 15:44:55 +0200 Subject: [PATCH] feat(CodeBuild): Ensure source repository URLs do not contain sensitive credentials (#4731) Co-authored-by: Sergio Garcia <38561120+sergargar@users.noreply.github.com> --- .../__init__.py | 0 ...url_no_sensitive_credentials.metadata.json | 32 ++++ ...ource_repo_url_no_sensitive_credentials.py | 44 +++++ .../services/codebuild/codebuild_service.py | 25 ++- ..._repo_url_no_sensitive_credentials_test.py | 165 ++++++++++++++++++ .../codebuild/codebuild_service_test.py | 21 ++- 6 files changed, 281 insertions(+), 6 deletions(-) create mode 100644 prowler/providers/aws/services/codebuild/codebuild_project_source_repo_url_no_sensitive_credentials/__init__.py create mode 100644 prowler/providers/aws/services/codebuild/codebuild_project_source_repo_url_no_sensitive_credentials/codebuild_project_source_repo_url_no_sensitive_credentials.metadata.json create mode 100644 prowler/providers/aws/services/codebuild/codebuild_project_source_repo_url_no_sensitive_credentials/codebuild_project_source_repo_url_no_sensitive_credentials.py create mode 100644 tests/providers/aws/services/codebuild/codebuild_project_source_repo_url_no_sensitive_credentials/codebuild_project_source_repo_url_no_sensitive_credentials_test.py diff --git a/prowler/providers/aws/services/codebuild/codebuild_project_source_repo_url_no_sensitive_credentials/__init__.py b/prowler/providers/aws/services/codebuild/codebuild_project_source_repo_url_no_sensitive_credentials/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/codebuild/codebuild_project_source_repo_url_no_sensitive_credentials/codebuild_project_source_repo_url_no_sensitive_credentials.metadata.json b/prowler/providers/aws/services/codebuild/codebuild_project_source_repo_url_no_sensitive_credentials/codebuild_project_source_repo_url_no_sensitive_credentials.metadata.json new file mode 100644 index 0000000000..5cc84590c9 --- /dev/null +++ b/prowler/providers/aws/services/codebuild/codebuild_project_source_repo_url_no_sensitive_credentials/codebuild_project_source_repo_url_no_sensitive_credentials.metadata.json @@ -0,0 +1,32 @@ +{ + "Provider": "aws", + "CheckID": "codebuild_project_source_repo_url_no_sensitive_credentials", + "CheckTitle": "Ensure CodeBuild project source repository URLs do not contain sensitive credentials", + "CheckType": [ + "Security Best Practices" + ], + "ServiceName": "codebuild", + "SubServiceName": "", + "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", + "Severity": "critical", + "ResourceType": "AwsCodeBuildProject", + "Description": "This check ensures an AWS CodeBuild project source repository URL doesn't contain personal access tokens or a user name and password. The check fails if the source repository URL contains personal access tokens or a user name and password.", + "Risk": "Storing or transmitting sign-in credentials in clear text or including them in the source repository URL can lead to unintended data exposure or unauthorized access, potentially compromising the security of the system.", + "RelatedUrl": "https://docs.aws.amazon.com/config/latest/developerguide/codebuild-project-source-repo-url-check.html", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/codebuild-controls.html#codebuild-1", + "Terraform": "" + }, + "Recommendation": { + "Text": "Update your CodeBuild project to use OAuth instead of personal access tokens or basic authentication in your repository URLs.", + "Url": "https://docs.aws.amazon.com/codebuild/latest/userguide/use-case-based-samples.html" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/codebuild/codebuild_project_source_repo_url_no_sensitive_credentials/codebuild_project_source_repo_url_no_sensitive_credentials.py b/prowler/providers/aws/services/codebuild/codebuild_project_source_repo_url_no_sensitive_credentials/codebuild_project_source_repo_url_no_sensitive_credentials.py new file mode 100644 index 0000000000..4473279a4f --- /dev/null +++ b/prowler/providers/aws/services/codebuild/codebuild_project_source_repo_url_no_sensitive_credentials/codebuild_project_source_repo_url_no_sensitive_credentials.py @@ -0,0 +1,44 @@ +import re + +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.codebuild.codebuild_client import codebuild_client + + +class codebuild_project_source_repo_url_no_sensitive_credentials(Check): + def execute(self): + findings = [] + token_pattern = re.compile(r"https://x-token-auth:[^@]+@bitbucket\.org/.+\.git") + user_pass_pattern = re.compile(r"https://[^:]+:[^@]+@bitbucket\.org/.+\.git") + for project in codebuild_client.projects.values(): + report = Check_Report_AWS(self.metadata()) + report.region = project.region + report.resource_id = project.name + report.resource_arn = project.arn + report.status = "PASS" + report.status_extended = f"CodeBuild project {project.name} does not contain sensitive credentials in any source repository URLs." + secrets_found = [] + + if project.source and project.source.type == "BITBUCKET": + if token_pattern.match(project.source.location): + secrets_found.append( + f"Token in {project.source.type} URL {project.source.location}" + ) + elif user_pass_pattern.match(project.source.location): + secrets_found.append( + f"Basic Auth Credentials in {project.source.type} URL {project.source.location}" + ) + for url in project.secondary_sources: + if url.type == "BITBUCKET": + if token_pattern.match(url.location): + secrets_found.append(f"Token in {url.type} URL {url.location}") + elif user_pass_pattern.match(url.location): + secrets_found.append( + f"Basic Auth Credentials in {url.type} URL {url.location}" + ) + if secrets_found: + report.status = "FAIL" + report.status_extended = f"CodeBuild project {project.name} has sensitive credentials in source repository URLs: {', '.join(secrets_found)}." + + 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 99ee7bfd62..541293f6fb 100644 --- a/prowler/providers/aws/services/codebuild/codebuild_service.py +++ b/prowler/providers/aws/services/codebuild/codebuild_service.py @@ -72,15 +72,27 @@ class Codebuild(AWSService): logger.info("Codebuild - Getting projects...") try: regional_client = self.regional_clients[project.region] - project_data = regional_client.batch_get_projects(names=[project.name])[ + project_info = regional_client.batch_get_projects(names=[project.name])[ "projects" ][0] - environment = project_data.get("environment", {}) + project.buildspec = project_info["source"].get("buildspec") + if project_info["source"]["type"] != "NO_SOURCE": + project.source = Source( + type=project_info["source"]["type"], + location=project_info["source"]["location"], + ) + project.secondary_sources = [] + for secondary_source in project_info.get("secondarySources", []): + source_obj = Source( + type=secondary_source["type"], location=secondary_source["location"] + ) + project.secondary_sources.append(source_obj) + environment = project_info.get("environment", {}) env_vars = environment.get("environmentVariables", []) project.environment_variables = [ EnvironmentVariable(**var) for var in env_vars ] - project.buildspec = project_data.get("source", {}).get("buildspec", "") + project.buildspec = project_info.get("source", {}).get("buildspec", "") except Exception as error: logger.error( f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" @@ -91,6 +103,11 @@ class Build(BaseModel): id: str +class Source(BaseModel): + type: str + location: str + + class EnvironmentVariable(BaseModel): name: str value: str @@ -104,4 +121,6 @@ class Project(BaseModel): last_build: Optional[Build] last_invoked_time: Optional[datetime.datetime] buildspec: Optional[str] + source: Optional[Source] + secondary_sources: Optional[list[Source]] = [] environment_variables: Optional[List[EnvironmentVariable]] diff --git a/tests/providers/aws/services/codebuild/codebuild_project_source_repo_url_no_sensitive_credentials/codebuild_project_source_repo_url_no_sensitive_credentials_test.py b/tests/providers/aws/services/codebuild/codebuild_project_source_repo_url_no_sensitive_credentials/codebuild_project_source_repo_url_no_sensitive_credentials_test.py new file mode 100644 index 0000000000..b3e7f1b967 --- /dev/null +++ b/tests/providers/aws/services/codebuild/codebuild_project_source_repo_url_no_sensitive_credentials/codebuild_project_source_repo_url_no_sensitive_credentials_test.py @@ -0,0 +1,165 @@ +from unittest import mock + +from prowler.providers.aws.services.codebuild.codebuild_service import Project, Source + +AWS_REGION = "eu-west-1" +AWS_ACCOUNT_NUMBER = "123456789012" + + +class Test_codebuild_project_source_repo_url_no_sensitive_credentials: + def test_project_no_bitbucket_urls(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", + last_invoked_time=None, + buildspec="", + source=None, + secondary_sources=[], + ) + } + with mock.patch( + "prowler.providers.aws.services.codebuild.codebuild_service.Codebuild", + codebuild_client, + ): + from prowler.providers.aws.services.codebuild.codebuild_project_source_repo_url_no_sensitive_credentials.codebuild_project_source_repo_url_no_sensitive_credentials import ( + codebuild_project_source_repo_url_no_sensitive_credentials, + ) + + check = codebuild_project_source_repo_url_no_sensitive_credentials() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"CodeBuild project {project_name} does not contain sensitive credentials in any source repository URLs." + ) + 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_safe_bitbucket_urls(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", + last_invoked_time=None, + buildspec=None, + source=Source( + type="BITBUCKET", + location="https://bitbucket.org/exampleuser/my-repo.git", + ), + secondary_sources=[], + ) + } + with mock.patch( + "prowler.providers.aws.services.codebuild.codebuild_service.Codebuild", + codebuild_client, + ): + from prowler.providers.aws.services.codebuild.codebuild_project_source_repo_url_no_sensitive_credentials.codebuild_project_source_repo_url_no_sensitive_credentials import ( + codebuild_project_source_repo_url_no_sensitive_credentials, + ) + + check = codebuild_project_source_repo_url_no_sensitive_credentials() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"CodeBuild project {project_name} does not contain sensitive credentials in any source repository URLs." + ) + 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_username_password_bitbucket_urls(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", + last_invoked_time=None, + buildspec="arn:aws:s3:::my-codebuild-sample2/buildspec.yaml", + source=Source( + type="BITBUCKET", + location="https://user:pass123@bitbucket.org/exampleuser/my-repo2.git", + ), + secondary_sources=[], + ) + } + with mock.patch( + "prowler.providers.aws.services.codebuild.codebuild_service.Codebuild", + codebuild_client, + ): + from prowler.providers.aws.services.codebuild.codebuild_project_source_repo_url_no_sensitive_credentials.codebuild_project_source_repo_url_no_sensitive_credentials import ( + codebuild_project_source_repo_url_no_sensitive_credentials, + ) + + check = codebuild_project_source_repo_url_no_sensitive_credentials() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"CodeBuild project {project_name} has sensitive credentials in source repository URLs: Basic Auth Credentials in BITBUCKET URL https://user:pass123@bitbucket.org/exampleuser/my-repo2.git." + ) + 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_token_bitbucket_urls(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", + last_invoked_time=None, + buildspec="arn:aws:s3:::my-codebuild-sample2/buildspec.yaml", + source=Source( + type="BITBUCKET", + location="https://x-token-auth:7saBEbfXpRg-zlO-YQC9Lvh8vtKmdETITD_-GCqYw0ZHbV7ZbMDbUCybDGM4=053EA782@bitbucket.org/testissue4244/test4244.git", + ), + secondary_sources=[], + ) + } + with mock.patch( + "prowler.providers.aws.services.codebuild.codebuild_service.Codebuild", + codebuild_client, + ): + from prowler.providers.aws.services.codebuild.codebuild_project_source_repo_url_no_sensitive_credentials.codebuild_project_source_repo_url_no_sensitive_credentials import ( + codebuild_project_source_repo_url_no_sensitive_credentials, + ) + + check = codebuild_project_source_repo_url_no_sensitive_credentials() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"CodeBuild project {project_name} has sensitive credentials in source repository URLs: Token in BITBUCKET URL https://x-token-auth:7saBEbfXpRg-zlO-YQC9Lvh8vtKmdETITD_-GCqYw0ZHbV7ZbMDbUCybDGM4=053EA782@bitbucket.org/testissue4244/test4244.git." + ) + 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 127102babb..0741f180c1 100644 --- a/tests/providers/aws/services/codebuild/codebuild_service_test.py +++ b/tests/providers/aws/services/codebuild/codebuild_service_test.py @@ -18,9 +18,11 @@ from tests.providers.aws.utils import ( project_name = "test" project_arn = f"arn:{AWS_COMMERCIAL_PARTITION}:codebuild:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:project/{project_name}" build_spec_project_arn = "arn:aws:s3:::my-codebuild-sample2/buildspec.yml" -buildspec_type = "S3" +source_type = "BITBUCKET" 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" # Mocking batch_get_projects make_api_call = botocore.client.BaseClient._make_api_call @@ -38,9 +40,17 @@ def mock_make_api_call(self, operation_name, kwarg): "projects": [ { "source": { - "type": buildspec_type, + "type": source_type, + "location": bitbucket_url, "buildspec": build_spec_project_arn, - } + }, + "secondarySources": [ + { + "type": source_type, + "location": secondary_bitbucket_url, + "buildspec": "", + } + ], } ] } @@ -78,3 +88,8 @@ class Test_Codebuild_Service: assert codebuild.projects[project_arn].last_invoked_time == last_invoked_time assert codebuild.projects[project_arn].last_build == Build(id=build_id) assert codebuild.projects[project_arn].buildspec == build_spec_project_arn + assert bitbucket_url == codebuild.projects[project_arn].source.location + assert ( + secondary_bitbucket_url + in codebuild.projects[project_arn].secondary_sources[0].location + )