diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index f341ee55ce..ab1d7ab3a5 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -7,6 +7,7 @@ All notable changes to the **Prowler SDK** are documented in this file. ### Added - `compute_instance_suspended_without_persistent_disks` check for GCP provider [(#9747)](https://github.com/prowler-cloud/prowler/pull/9747) +- `codebuild_project_webhook_filters_use_anchored_patterns` check for AWS provider to detect CodeBreach vulnerability [(#9840)](https://github.com/prowler-cloud/prowler/pull/9840) ### Changed diff --git a/prowler/providers/aws/services/codebuild/codebuild_project_webhook_filters_use_anchored_patterns/__init__.py b/prowler/providers/aws/services/codebuild/codebuild_project_webhook_filters_use_anchored_patterns/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/codebuild/codebuild_project_webhook_filters_use_anchored_patterns/codebuild_project_webhook_filters_use_anchored_patterns.metadata.json b/prowler/providers/aws/services/codebuild/codebuild_project_webhook_filters_use_anchored_patterns/codebuild_project_webhook_filters_use_anchored_patterns.metadata.json new file mode 100644 index 0000000000..f83824d81b --- /dev/null +++ b/prowler/providers/aws/services/codebuild/codebuild_project_webhook_filters_use_anchored_patterns/codebuild_project_webhook_filters_use_anchored_patterns.metadata.json @@ -0,0 +1,40 @@ +{ + "Provider": "aws", + "CheckID": "codebuild_project_webhook_filters_use_anchored_patterns", + "CheckTitle": "CodeBuild project webhook filters use anchored regex patterns", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices" + ], + "ServiceName": "codebuild", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "high", + "ResourceType": "AwsCodeBuildProject", + "ResourceGroup": "devops", + "Description": "AWS CodeBuild webhook filters using `ACTOR_ACCOUNT_ID`, `HEAD_REF`, or `BASE_REF` have regex patterns anchored with `^` (start) and `$` (end) to enforce exact matching and prevent substring bypass attacks.", + "Risk": "Unanchored patterns expose CI/CD pipelines to **CodeBreach** attacks. Attackers can bypass `ACTOR_ACCOUNT_ID` filters by creating GitHub accounts with IDs containing trusted values as substrings. **Confidentiality**: Credentials leaked via build logs. **Integrity**: Malicious code injected into builds. **Availability**: Resource exhaustion through unauthorized builds.", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "aws codebuild update-webhook --project-name --filter-groups '[[{\"type\":\"ACTOR_ACCOUNT_ID\",\"pattern\":\"^123456$|^234567$\"}]]'", + "NativeIaC": "AWSTemplateFormatVersion: '2010-09-09'\nResources:\n CodeBuildWebhook:\n Type: AWS::CodeBuild::Project\n Properties:\n Triggers:\n Webhook: true\n FilterGroups:\n - - Type: ACTOR_ACCOUNT_ID\n Pattern: '^123456$|^234567$' # Anchored pattern", + "Other": "1. Open AWS Console and navigate to CodeBuild. 2. Select the project with webhook filters. 3. Click Edit and go to Primary source webhook events. 4. For each filter using ACTOR_ACCOUNT_ID, HEAD_REF, or BASE_REF, update patterns to include ^ at start and $ at end (e.g., change '123456|234567' to '^123456$|^234567$'). 5. Save changes.", + "Terraform": "resource \"aws_codebuild_webhook\" \"example\" {\n project_name = aws_codebuild_project.example.name\n filter_group {\n filter {\n type = \"ACTOR_ACCOUNT_ID\"\n pattern = \"^123456$|^234567$\" # Anchored pattern\n }\n }\n}" + }, + "Recommendation": { + "Text": "Anchor all webhook filter patterns with `^` (start) and `$` (end) to enforce exact matching. For multiple values use: `^value1$|^value2$`. This prevents attackers from bypassing filters using substring matches.", + "Url": "https://hub.prowler.com/check/codebuild_project_webhook_filters_use_anchored_patterns" + } + }, + "Categories": [ + "software-supply-chain", + "ci-cd" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "This check targets the CodeBreach vulnerability disclosed by Wiz Research. The vulnerability allows attackers to bypass ACTOR_ACCOUNT_ID filters by creating GitHub accounts with IDs that contain trusted IDs as substrings.", + "AdditionalURLs": [ + "https://www.wiz.io/blog/wiz-research-codebreach-vulnerability-aws-codebuild", + "https://docs.aws.amazon.com/codebuild/latest/userguide/github-webhook.html" + ] +} diff --git a/prowler/providers/aws/services/codebuild/codebuild_project_webhook_filters_use_anchored_patterns/codebuild_project_webhook_filters_use_anchored_patterns.py b/prowler/providers/aws/services/codebuild/codebuild_project_webhook_filters_use_anchored_patterns/codebuild_project_webhook_filters_use_anchored_patterns.py new file mode 100644 index 0000000000..231e392687 --- /dev/null +++ b/prowler/providers/aws/services/codebuild/codebuild_project_webhook_filters_use_anchored_patterns/codebuild_project_webhook_filters_use_anchored_patterns.py @@ -0,0 +1,58 @@ +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 + +HIGH_RISK_FILTER_TYPES = {"ACTOR_ACCOUNT_ID", "HEAD_REF", "BASE_REF"} + + +def is_pattern_anchored(pattern: str) -> bool: + """Check if each alternative in a pipe-separated pattern is anchored with ^ and $.""" + if not pattern: + return True + + for alt in pattern.split("|"): + alt = alt.strip() + if alt and not (alt.startswith("^") and alt.endswith("$")): + return False + return True + + +class codebuild_project_webhook_filters_use_anchored_patterns(Check): + def execute(self) -> List[Check_Report_AWS]: + findings = [] + + for project in codebuild_client.projects.values(): + report = Check_Report_AWS(metadata=self.metadata(), resource=project) + report.status = "PASS" + report.status_extended = ( + f"CodeBuild project {project.name} has no webhook configured or all " + "webhook filter patterns are properly anchored." + ) + + if not project.webhook or not project.webhook.filter_groups: + findings.append(report) + continue + + unanchored_filters = [] + for filter_group in project.webhook.filter_groups: + for webhook_filter in filter_group.filters: + if webhook_filter.type in HIGH_RISK_FILTER_TYPES: + if not is_pattern_anchored(webhook_filter.pattern): + unanchored_filters.append( + f"{webhook_filter.type}: '{webhook_filter.pattern}'" + ) + + if unanchored_filters: + report.status = "FAIL" + filters_str = ", ".join(unanchored_filters[:3]) + if len(unanchored_filters) > 3: + filters_str += f" and {len(unanchored_filters) - 3} more" + report.status_extended = ( + f"CodeBuild project {project.name} has webhook filters with " + f"unanchored patterns that could allow bypass attacks: {filters_str}." + ) + + 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 475f578e94..361002aa65 100644 --- a/prowler/providers/aws/services/codebuild/codebuild_service.py +++ b/prowler/providers/aws/services/codebuild/codebuild_service.py @@ -122,6 +122,29 @@ class Codebuild(AWSService): project.tags = project_info.get("tags", []) project.service_role_arn = project_info.get("serviceRole", "") project.project_visibility = project_info.get("projectVisibility", "") + + # Extract webhook configuration + webhook_data = project_info.get("webhook") + if webhook_data: + filter_groups = [] + for fg in webhook_data.get("filterGroups", []): + filters = [] + for f in fg: + filters.append( + WebhookFilter( + type=f.get("type", ""), + pattern=f.get("pattern", ""), + exclude_matched_pattern=f.get( + "excludeMatchedPattern", False + ), + ) + ) + filter_groups.append(WebhookFilterGroup(filters=filters)) + + project.webhook = Webhook( + filter_groups=filter_groups, + branch_filter=webhook_data.get("branchFilter"), + ) except Exception as error: logger.error( f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" @@ -209,6 +232,27 @@ class CloudWatchLogs(BaseModel): stream_name: str +class WebhookFilter(BaseModel): + """Represents a single filter in a webhook filter group.""" + + type: str # ACTOR_ACCOUNT_ID, HEAD_REF, BASE_REF, EVENT, etc. + pattern: str + exclude_matched_pattern: bool = False + + +class WebhookFilterGroup(BaseModel): + """Represents a group of filters (AND logic within group).""" + + filters: List[WebhookFilter] = [] + + +class Webhook(BaseModel): + """Represents the webhook configuration for a CodeBuild project.""" + + filter_groups: List[WebhookFilterGroup] = [] + branch_filter: Optional[str] = None + + class Project(BaseModel): name: str arn: str @@ -224,6 +268,7 @@ class Project(BaseModel): cloudwatch_logs: Optional[CloudWatchLogs] tags: Optional[list] project_visibility: Optional[str] = None + webhook: Optional[Webhook] = None class ExportConfig(BaseModel): diff --git a/tests/providers/aws/services/codebuild/codebuild_project_webhook_filters_use_anchored_patterns/codebuild_project_webhook_filters_use_anchored_patterns_test.py b/tests/providers/aws/services/codebuild/codebuild_project_webhook_filters_use_anchored_patterns/codebuild_project_webhook_filters_use_anchored_patterns_test.py new file mode 100644 index 0000000000..e8b370ad49 --- /dev/null +++ b/tests/providers/aws/services/codebuild/codebuild_project_webhook_filters_use_anchored_patterns/codebuild_project_webhook_filters_use_anchored_patterns_test.py @@ -0,0 +1,667 @@ +from unittest import mock + +from prowler.providers.aws.services.codebuild.codebuild_service import ( + Project, + Webhook, + WebhookFilter, + WebhookFilterGroup, +) + +AWS_REGION = "eu-west-1" +AWS_ACCOUNT_NUMBER = "123456789012" + + +class Test_codebuild_project_webhook_filters_use_anchored_patterns: + def test_no_projects(self): + codebuild_client = mock.MagicMock + codebuild_client.projects = {} + + with ( + mock.patch( + "prowler.providers.aws.services.codebuild.codebuild_service.Codebuild", + codebuild_client, + ), + mock.patch( + "prowler.providers.aws.services.codebuild.codebuild_project_webhook_filters_use_anchored_patterns.codebuild_project_webhook_filters_use_anchored_patterns.codebuild_client", + codebuild_client, + ), + ): + from prowler.providers.aws.services.codebuild.codebuild_project_webhook_filters_use_anchored_patterns.codebuild_project_webhook_filters_use_anchored_patterns import ( + codebuild_project_webhook_filters_use_anchored_patterns, + ) + + check = codebuild_project_webhook_filters_use_anchored_patterns() + result = check.execute() + + assert len(result) == 0 + + def test_project_without_webhook(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=AWS_REGION, + webhook=None, + tags=[], + ) + } + + with ( + mock.patch( + "prowler.providers.aws.services.codebuild.codebuild_service.Codebuild", + codebuild_client, + ), + mock.patch( + "prowler.providers.aws.services.codebuild.codebuild_project_webhook_filters_use_anchored_patterns.codebuild_project_webhook_filters_use_anchored_patterns.codebuild_client", + codebuild_client, + ), + ): + from prowler.providers.aws.services.codebuild.codebuild_project_webhook_filters_use_anchored_patterns.codebuild_project_webhook_filters_use_anchored_patterns import ( + codebuild_project_webhook_filters_use_anchored_patterns, + ) + + check = codebuild_project_webhook_filters_use_anchored_patterns() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + "no webhook configured or all webhook filter patterns are properly anchored" + in result[0].status_extended + ) + assert result[0].resource_id == project_name + assert result[0].resource_arn == project_arn + assert result[0].region == AWS_REGION + + def test_project_webhook_empty_filter_groups(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=AWS_REGION, + webhook=Webhook(filter_groups=[]), + tags=[], + ) + } + + with ( + mock.patch( + "prowler.providers.aws.services.codebuild.codebuild_service.Codebuild", + codebuild_client, + ), + mock.patch( + "prowler.providers.aws.services.codebuild.codebuild_project_webhook_filters_use_anchored_patterns.codebuild_project_webhook_filters_use_anchored_patterns.codebuild_client", + codebuild_client, + ), + ): + from prowler.providers.aws.services.codebuild.codebuild_project_webhook_filters_use_anchored_patterns.codebuild_project_webhook_filters_use_anchored_patterns import ( + codebuild_project_webhook_filters_use_anchored_patterns, + ) + + check = codebuild_project_webhook_filters_use_anchored_patterns() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + "no webhook configured or all webhook filter patterns are properly anchored" + in result[0].status_extended + ) + + def test_project_webhook_with_anchored_patterns(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=AWS_REGION, + webhook=Webhook( + filter_groups=[ + WebhookFilterGroup( + filters=[ + WebhookFilter( + type="ACTOR_ACCOUNT_ID", + pattern="^123456789$|^987654321$", + ), + WebhookFilter( + type="HEAD_REF", + pattern="^refs/heads/main$", + ), + ] + ) + ] + ), + tags=[], + ) + } + + with ( + mock.patch( + "prowler.providers.aws.services.codebuild.codebuild_service.Codebuild", + codebuild_client, + ), + mock.patch( + "prowler.providers.aws.services.codebuild.codebuild_project_webhook_filters_use_anchored_patterns.codebuild_project_webhook_filters_use_anchored_patterns.codebuild_client", + codebuild_client, + ), + ): + from prowler.providers.aws.services.codebuild.codebuild_project_webhook_filters_use_anchored_patterns.codebuild_project_webhook_filters_use_anchored_patterns import ( + codebuild_project_webhook_filters_use_anchored_patterns, + ) + + check = codebuild_project_webhook_filters_use_anchored_patterns() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + "no webhook configured or all webhook filter patterns are properly anchored" + in result[0].status_extended + ) + assert result[0].resource_id == project_name + assert result[0].resource_arn == project_arn + assert result[0].region == AWS_REGION + + def test_project_webhook_with_unanchored_patterns(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=AWS_REGION, + webhook=Webhook( + filter_groups=[ + WebhookFilterGroup( + filters=[ + WebhookFilter( + type="ACTOR_ACCOUNT_ID", + pattern="123456|234567", + ), + ] + ) + ] + ), + tags=[], + ) + } + + with ( + mock.patch( + "prowler.providers.aws.services.codebuild.codebuild_service.Codebuild", + codebuild_client, + ), + mock.patch( + "prowler.providers.aws.services.codebuild.codebuild_project_webhook_filters_use_anchored_patterns.codebuild_project_webhook_filters_use_anchored_patterns.codebuild_client", + codebuild_client, + ), + ): + from prowler.providers.aws.services.codebuild.codebuild_project_webhook_filters_use_anchored_patterns.codebuild_project_webhook_filters_use_anchored_patterns import ( + codebuild_project_webhook_filters_use_anchored_patterns, + ) + + check = codebuild_project_webhook_filters_use_anchored_patterns() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert "unanchored patterns" in result[0].status_extended + assert "ACTOR_ACCOUNT_ID" in result[0].status_extended + assert result[0].resource_id == project_name + assert result[0].resource_arn == project_arn + assert result[0].region == AWS_REGION + + def test_project_webhook_with_mixed_anchored_unanchored(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=AWS_REGION, + webhook=Webhook( + filter_groups=[ + WebhookFilterGroup( + filters=[ + WebhookFilter( + type="ACTOR_ACCOUNT_ID", + pattern="^123456$|234567", + ), + ] + ) + ] + ), + tags=[], + ) + } + + with ( + mock.patch( + "prowler.providers.aws.services.codebuild.codebuild_service.Codebuild", + codebuild_client, + ), + mock.patch( + "prowler.providers.aws.services.codebuild.codebuild_project_webhook_filters_use_anchored_patterns.codebuild_project_webhook_filters_use_anchored_patterns.codebuild_client", + codebuild_client, + ), + ): + from prowler.providers.aws.services.codebuild.codebuild_project_webhook_filters_use_anchored_patterns.codebuild_project_webhook_filters_use_anchored_patterns import ( + codebuild_project_webhook_filters_use_anchored_patterns, + ) + + check = codebuild_project_webhook_filters_use_anchored_patterns() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert "unanchored patterns" in result[0].status_extended + + def test_project_multiple_filter_groups_one_bad(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=AWS_REGION, + webhook=Webhook( + filter_groups=[ + WebhookFilterGroup( + filters=[ + WebhookFilter( + type="ACTOR_ACCOUNT_ID", + pattern="^123456789$", + ), + ] + ), + WebhookFilterGroup( + filters=[ + WebhookFilter( + type="BASE_REF", + pattern="refs/heads/main", + ), + ] + ), + ] + ), + tags=[], + ) + } + + with ( + mock.patch( + "prowler.providers.aws.services.codebuild.codebuild_service.Codebuild", + codebuild_client, + ), + mock.patch( + "prowler.providers.aws.services.codebuild.codebuild_project_webhook_filters_use_anchored_patterns.codebuild_project_webhook_filters_use_anchored_patterns.codebuild_client", + codebuild_client, + ), + ): + from prowler.providers.aws.services.codebuild.codebuild_project_webhook_filters_use_anchored_patterns.codebuild_project_webhook_filters_use_anchored_patterns import ( + codebuild_project_webhook_filters_use_anchored_patterns, + ) + + check = codebuild_project_webhook_filters_use_anchored_patterns() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert "BASE_REF" in result[0].status_extended + assert "unanchored patterns" in result[0].status_extended + + def test_project_non_high_risk_filter_unanchored(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=AWS_REGION, + webhook=Webhook( + filter_groups=[ + WebhookFilterGroup( + filters=[ + WebhookFilter( + type="EVENT", + pattern="PUSH|PULL_REQUEST_MERGED", + ), + ] + ) + ] + ), + tags=[], + ) + } + + with ( + mock.patch( + "prowler.providers.aws.services.codebuild.codebuild_service.Codebuild", + codebuild_client, + ), + mock.patch( + "prowler.providers.aws.services.codebuild.codebuild_project_webhook_filters_use_anchored_patterns.codebuild_project_webhook_filters_use_anchored_patterns.codebuild_client", + codebuild_client, + ), + ): + from prowler.providers.aws.services.codebuild.codebuild_project_webhook_filters_use_anchored_patterns.codebuild_project_webhook_filters_use_anchored_patterns import ( + codebuild_project_webhook_filters_use_anchored_patterns, + ) + + check = codebuild_project_webhook_filters_use_anchored_patterns() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + "no webhook configured or all webhook filter patterns are properly anchored" + in result[0].status_extended + ) + + def test_project_multiple_unanchored_filters_truncated(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=AWS_REGION, + webhook=Webhook( + filter_groups=[ + WebhookFilterGroup( + filters=[ + WebhookFilter( + type="ACTOR_ACCOUNT_ID", + pattern="123456", + ), + WebhookFilter( + type="HEAD_REF", + pattern="refs/heads/main", + ), + WebhookFilter( + type="BASE_REF", + pattern="refs/heads/develop", + ), + WebhookFilter( + type="ACTOR_ACCOUNT_ID", + pattern="987654", + ), + ] + ) + ] + ), + tags=[], + ) + } + + with ( + mock.patch( + "prowler.providers.aws.services.codebuild.codebuild_service.Codebuild", + codebuild_client, + ), + mock.patch( + "prowler.providers.aws.services.codebuild.codebuild_project_webhook_filters_use_anchored_patterns.codebuild_project_webhook_filters_use_anchored_patterns.codebuild_client", + codebuild_client, + ), + ): + from prowler.providers.aws.services.codebuild.codebuild_project_webhook_filters_use_anchored_patterns.codebuild_project_webhook_filters_use_anchored_patterns import ( + codebuild_project_webhook_filters_use_anchored_patterns, + ) + + check = codebuild_project_webhook_filters_use_anchored_patterns() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert "and 1 more" in result[0].status_extended + + def test_project_webhook_with_empty_pattern(self): + """Empty patterns should PASS as they don't pose a bypass risk.""" + 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=AWS_REGION, + webhook=Webhook( + filter_groups=[ + WebhookFilterGroup( + filters=[ + WebhookFilter( + type="ACTOR_ACCOUNT_ID", + pattern="", + ), + ] + ) + ] + ), + tags=[], + ) + } + + with ( + mock.patch( + "prowler.providers.aws.services.codebuild.codebuild_service.Codebuild", + codebuild_client, + ), + mock.patch( + "prowler.providers.aws.services.codebuild.codebuild_project_webhook_filters_use_anchored_patterns.codebuild_project_webhook_filters_use_anchored_patterns.codebuild_client", + codebuild_client, + ), + ): + from prowler.providers.aws.services.codebuild.codebuild_project_webhook_filters_use_anchored_patterns.codebuild_project_webhook_filters_use_anchored_patterns import ( + codebuild_project_webhook_filters_use_anchored_patterns, + ) + + check = codebuild_project_webhook_filters_use_anchored_patterns() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + + def test_project_webhook_with_start_anchor_only(self): + """Pattern with only start anchor (^) should FAIL.""" + 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=AWS_REGION, + webhook=Webhook( + filter_groups=[ + WebhookFilterGroup( + filters=[ + WebhookFilter( + type="ACTOR_ACCOUNT_ID", + pattern="^123456789", + ), + ] + ) + ] + ), + tags=[], + ) + } + + with ( + mock.patch( + "prowler.providers.aws.services.codebuild.codebuild_service.Codebuild", + codebuild_client, + ), + mock.patch( + "prowler.providers.aws.services.codebuild.codebuild_project_webhook_filters_use_anchored_patterns.codebuild_project_webhook_filters_use_anchored_patterns.codebuild_client", + codebuild_client, + ), + ): + from prowler.providers.aws.services.codebuild.codebuild_project_webhook_filters_use_anchored_patterns.codebuild_project_webhook_filters_use_anchored_patterns import ( + codebuild_project_webhook_filters_use_anchored_patterns, + ) + + check = codebuild_project_webhook_filters_use_anchored_patterns() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert "unanchored patterns" in result[0].status_extended + + def test_project_webhook_with_end_anchor_only(self): + """Pattern with only end anchor ($) should FAIL.""" + 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=AWS_REGION, + webhook=Webhook( + filter_groups=[ + WebhookFilterGroup( + filters=[ + WebhookFilter( + type="ACTOR_ACCOUNT_ID", + pattern="123456789$", + ), + ] + ) + ] + ), + tags=[], + ) + } + + with ( + mock.patch( + "prowler.providers.aws.services.codebuild.codebuild_service.Codebuild", + codebuild_client, + ), + mock.patch( + "prowler.providers.aws.services.codebuild.codebuild_project_webhook_filters_use_anchored_patterns.codebuild_project_webhook_filters_use_anchored_patterns.codebuild_client", + codebuild_client, + ), + ): + from prowler.providers.aws.services.codebuild.codebuild_project_webhook_filters_use_anchored_patterns.codebuild_project_webhook_filters_use_anchored_patterns import ( + codebuild_project_webhook_filters_use_anchored_patterns, + ) + + check = codebuild_project_webhook_filters_use_anchored_patterns() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert "unanchored patterns" in result[0].status_extended + + def test_project_webhook_codebreach_research_vulnerable_pattern(self): + """Test with the exact vulnerable pattern from Wiz CodeBreach research - should FAIL.""" + 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=AWS_REGION, + webhook=Webhook( + filter_groups=[ + WebhookFilterGroup( + filters=[ + WebhookFilter( + type="ACTOR_ACCOUNT_ID", + pattern="16024985|755743|48153483|191175973|47447266|213081198", + ), + ] + ) + ] + ), + tags=[], + ) + } + + with ( + mock.patch( + "prowler.providers.aws.services.codebuild.codebuild_service.Codebuild", + codebuild_client, + ), + mock.patch( + "prowler.providers.aws.services.codebuild.codebuild_project_webhook_filters_use_anchored_patterns.codebuild_project_webhook_filters_use_anchored_patterns.codebuild_client", + codebuild_client, + ), + ): + from prowler.providers.aws.services.codebuild.codebuild_project_webhook_filters_use_anchored_patterns.codebuild_project_webhook_filters_use_anchored_patterns import ( + codebuild_project_webhook_filters_use_anchored_patterns, + ) + + check = codebuild_project_webhook_filters_use_anchored_patterns() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert "unanchored patterns" in result[0].status_extended + assert "ACTOR_ACCOUNT_ID" in result[0].status_extended + + def test_project_webhook_codebreach_research_fixed_pattern(self): + """Test with the properly anchored version of the research pattern - should PASS.""" + 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=AWS_REGION, + webhook=Webhook( + filter_groups=[ + WebhookFilterGroup( + filters=[ + WebhookFilter( + type="ACTOR_ACCOUNT_ID", + pattern="^16024985$|^755743$|^48153483$|^191175973$|^47447266$|^213081198$", + ), + ] + ) + ] + ), + tags=[], + ) + } + + with ( + mock.patch( + "prowler.providers.aws.services.codebuild.codebuild_service.Codebuild", + codebuild_client, + ), + mock.patch( + "prowler.providers.aws.services.codebuild.codebuild_project_webhook_filters_use_anchored_patterns.codebuild_project_webhook_filters_use_anchored_patterns.codebuild_client", + codebuild_client, + ), + ): + from prowler.providers.aws.services.codebuild.codebuild_project_webhook_filters_use_anchored_patterns.codebuild_project_webhook_filters_use_anchored_patterns import ( + codebuild_project_webhook_filters_use_anchored_patterns, + ) + + check = codebuild_project_webhook_filters_use_anchored_patterns() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + "no webhook configured or all webhook filter patterns are properly anchored" + in result[0].status_extended + ) diff --git a/tests/providers/aws/services/codebuild/codebuild_service_test.py b/tests/providers/aws/services/codebuild/codebuild_service_test.py index 9884b3a09f..6bf81f58c2 100644 --- a/tests/providers/aws/services/codebuild/codebuild_service_test.py +++ b/tests/providers/aws/services/codebuild/codebuild_service_test.py @@ -11,6 +11,9 @@ from prowler.providers.aws.services.codebuild.codebuild_service import ( ExportConfig, Project, ReportGroup, + Webhook, + WebhookFilter, + WebhookFilterGroup, s3Logs, ) from tests.providers.aws.utils import ( @@ -73,6 +76,23 @@ def mock_make_api_call(self, operation_name, kwarg): }, "tags": [{"key": "Name", "value": project_name}], "projectVisibility": project_visibility, + "webhook": { + "filterGroups": [ + [ + { + "type": "ACTOR_ACCOUNT_ID", + "pattern": "^123456789$", + "excludeMatchedPattern": False, + }, + { + "type": "EVENT", + "pattern": "PUSH", + "excludeMatchedPattern": False, + }, + ] + ], + "branchFilter": "main", + }, } ] } @@ -155,7 +175,37 @@ 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 + # Assertions related with webhooks + assert codebuild.projects[project_arn].webhook is not None + assert isinstance(codebuild.projects[project_arn].webhook, Webhook) + assert codebuild.projects[project_arn].webhook.branch_filter == "main" + assert len(codebuild.projects[project_arn].webhook.filter_groups) == 1 + assert isinstance( + codebuild.projects[project_arn].webhook.filter_groups[0], WebhookFilterGroup + ) + assert ( + len(codebuild.projects[project_arn].webhook.filter_groups[0].filters) == 2 + ) + assert isinstance( + codebuild.projects[project_arn].webhook.filter_groups[0].filters[0], + WebhookFilter, + ) + assert ( + codebuild.projects[project_arn].webhook.filter_groups[0].filters[0].type + == "ACTOR_ACCOUNT_ID" + ) + assert ( + codebuild.projects[project_arn].webhook.filter_groups[0].filters[0].pattern + == "^123456789$" + ) + assert ( + codebuild.projects[project_arn] + .webhook.filter_groups[0] + .filters[0] + .exclude_matched_pattern + is False + ) + # Assertions related with report groups assert len(codebuild.report_groups) == 1 assert isinstance(codebuild.report_groups, dict) assert isinstance(codebuild.report_groups[report_group_arn], ReportGroup)