from unittest.mock import MagicMock, patch import botocore from botocore.exceptions import ClientError from moto import mock_aws from prowler.providers.aws.services.codecommit.codecommit_service import ( CodeCommit, Repository, ) from tests.providers.aws.utils import ( AWS_ACCOUNT_NUMBER, AWS_COMMERCIAL_PARTITION, AWS_REGION_EU_WEST_1, set_mocked_aws_provider, ) repository_name = "test-repo" repository_id = "repo-id-1234" repository_arn = f"arn:{AWS_COMMERCIAL_PARTITION}:codecommit:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:{repository_name}" default_branch = "main" commit_id = "commit-1234" # Mocking API calls make_api_call = botocore.client.BaseClient._make_api_call def mock_make_api_call(self, operation_name, kwarg): if operation_name == "ListRepositories": return { "repositories": [ {"repositoryName": repository_name, "repositoryId": repository_id} ] } elif operation_name == "GetRepository": return { "repositoryMetadata": { "repositoryId": repository_id, "repositoryName": repository_name, "defaultBranch": default_branch, } } elif operation_name == "GetBranch": return {"branch": {"branchName": default_branch, "commitId": commit_id}} elif operation_name == "ListTagsForResource": return {"tags": {"Environment": "Test"}} elif operation_name == "GetFolder": if kwarg["folderPath"] == "/": return { "commitId": commit_id, "folderPath": "/", "subFolders": [{"absolutePath": "/src"}], "files": [{"absolutePath": "README.md", "blobId": "blob-readme"}], } elif kwarg["folderPath"] == "/src": return { "commitId": commit_id, "folderPath": "/src", "subFolders": [], "files": [ {"absolutePath": "/src/secrets.py", "blobId": "blob-secrets"} ], } elif operation_name == "GetBlob": if kwarg["blobId"] == "blob-readme": return {"content": b"# Test repository\n"} elif kwarg["blobId"] == "blob-secrets": return {"content": b'AWS_ACCESS_KEY_ID = "AKIAIOSFODNN7EXAMPLE"\n'} return make_api_call(self, operation_name, kwarg) # Mock generate_regional_clients() def mock_generate_regional_clients(provider, service): regional_client = provider._session.current_session.client( service, region_name=AWS_REGION_EU_WEST_1 ) regional_client.region = AWS_REGION_EU_WEST_1 return {AWS_REGION_EU_WEST_1: regional_client} def mock_make_api_call_list_repositories_access_denied(self, operation_name, kwarg): if operation_name == "ListRepositories": # CodeCommit is a JSON-protocol API, so real IAM denials surface as # AccessDeniedException (the service also accepts plain AccessDenied). raise ClientError( {"Error": {"Code": "AccessDeniedException", "Message": "Access Denied"}}, operation_name, ) return make_api_call(self, operation_name, kwarg) def mock_make_api_call_list_repositories_client_error(self, operation_name, kwarg): if operation_name == "ListRepositories": raise ClientError( {"Error": {"Code": "ThrottlingException", "Message": "Rate exceeded"}}, operation_name, ) return make_api_call(self, operation_name, kwarg) def mock_make_api_call_repository_errors(self, operation_name, kwarg): if operation_name == "ListRepositories": return { "repositories": [ {"repositoryName": repository_name, "repositoryId": repository_id}, {"repositoryName": "repo-not-exist", "repositoryId": "repo-id-2"}, {"repositoryName": "repo-other-error", "repositoryId": "repo-id-3"}, {"repositoryName": "repo-branch-error", "repositoryId": "repo-id-4"}, ] } elif operation_name == "GetRepository": name = kwarg["repositoryName"] if name == "repo-not-exist": raise ClientError( { "Error": { "Code": "RepositoryDoesNotExistException", "Message": "Repository does not exist", } }, operation_name, ) if name == "repo-other-error": raise ClientError( {"Error": {"Code": "InternalServerException", "Message": "Boom"}}, operation_name, ) return { "repositoryMetadata": { "repositoryId": name, "repositoryName": name, "defaultBranch": default_branch, } } elif operation_name == "GetBranch": if kwarg["repositoryName"] == "repo-branch-error": raise ClientError( {"Error": {"Code": "InternalServerException", "Message": "Boom"}}, operation_name, ) return {"branch": {"branchName": default_branch, "commitId": commit_id}} elif operation_name == "ListTagsForResource": return {"tags": {}} return make_api_call(self, operation_name, kwarg) def mock_make_api_call_tags_errors(self, operation_name, kwarg): if operation_name == "ListRepositories": return { "repositories": [ {"repositoryName": "tags-not-found", "repositoryId": "repo-id-1"}, {"repositoryName": "tags-other-error", "repositoryId": "repo-id-2"}, ] } elif operation_name == "GetRepository": name = kwarg["repositoryName"] return { "repositoryMetadata": { "repositoryId": name, "repositoryName": name, "defaultBranch": default_branch, } } elif operation_name == "GetBranch": return {"branch": {"branchName": default_branch, "commitId": commit_id}} elif operation_name == "ListTagsForResource": if "tags-not-found" in kwarg["resourceArn"]: raise ClientError( { "Error": { "Code": "ResourceNotFoundException", "Message": "Not found", } }, operation_name, ) if "tags-other-error" in kwarg["resourceArn"]: raise ClientError( {"Error": {"Code": "InternalServerException", "Message": "Boom"}}, operation_name, ) return make_api_call(self, operation_name, kwarg) def mock_make_api_call_files_errors(self, operation_name, kwarg): if operation_name == "ListRepositories": return { "repositories": [ {"repositoryName": repository_name, "repositoryId": repository_id} ] } elif operation_name == "GetRepository": return { "repositoryMetadata": { "repositoryId": repository_id, "repositoryName": repository_name, "defaultBranch": default_branch, } } elif operation_name == "GetBranch": return {"branch": {"branchName": default_branch, "commitId": commit_id}} elif operation_name == "ListTagsForResource": return {"tags": {}} elif operation_name == "GetFolder": if kwarg["folderPath"] == "/": return { "commitId": commit_id, "folderPath": "/", "subFolders": [ {"absolutePath": "/broken-folder"}, {"absolutePath": "/broken-folder-2"}, {"absolutePath": "/src"}, ], "files": [ {"absolutePath": "README.md", "blobId": "blob-readme"}, {"absolutePath": "bad-blob.txt", "blobId": "blob-bad"}, {"absolutePath": "error-blob.txt", "blobId": "blob-error"}, ], } elif kwarg["folderPath"] == "/broken-folder": raise ClientError( { "Error": { "Code": "EncryptionKeyAccessDeniedException", "Message": "Access denied", } }, operation_name, ) elif kwarg["folderPath"] == "/broken-folder-2": raise Exception("Generic folder error") elif kwarg["folderPath"] == "/src": return { "commitId": commit_id, "folderPath": "/src", "subFolders": [], "files": [ {"absolutePath": "/src/secrets.py", "blobId": "blob-secrets"} ], } elif operation_name == "GetBlob": if kwarg["blobId"] == "blob-readme": return {"content": b"# Test repository\n"} elif kwarg["blobId"] == "blob-secrets": return {"content": b'AWS_ACCESS_KEY_ID = "AKIAIOSFODNN7EXAMPLE"\n'} elif kwarg["blobId"] == "blob-bad": raise ClientError( { "Error": { "Code": "BlobIdDoesNotExistException", "Message": "Blob not found", } }, operation_name, ) elif kwarg["blobId"] == "blob-error": raise Exception("Generic blob error") return make_api_call(self, operation_name, kwarg) class Test_CodeCommit_Service: @patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call) @patch( "prowler.providers.aws.aws_provider.AwsProvider.generate_regional_clients", new=mock_generate_regional_clients, ) @mock_aws def test_codecommit_service(self): codecommit = CodeCommit(set_mocked_aws_provider([AWS_REGION_EU_WEST_1])) assert codecommit.session.__class__.__name__ == "Session" assert codecommit.service == "codecommit" # Test repository properties assert len(codecommit.repositories) == 1 assert isinstance(codecommit.repositories, dict) assert isinstance(codecommit.repositories[repository_arn], Repository) repository = codecommit.repositories[repository_arn] assert repository.repository_id == repository_id assert repository.name == repository_name assert repository.arn == repository_arn assert repository.region == AWS_REGION_EU_WEST_1 assert repository.default_branch == default_branch assert repository.default_branch_commit_id == commit_id # Test tags assert repository.tags == {"Environment": "Test"} @patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call) @patch( "prowler.providers.aws.aws_provider.AwsProvider.generate_regional_clients", new=mock_generate_regional_clients, ) @mock_aws def test_get_repository_files_content(self): codecommit = CodeCommit(set_mocked_aws_provider([AWS_REGION_EU_WEST_1])) repository = codecommit.repositories[repository_arn] files = dict(codecommit.get_repository_files_content(repository)) assert files == { "README.md": b"# Test repository\n", "/src/secrets.py": b'AWS_ACCESS_KEY_ID = "AKIAIOSFODNN7EXAMPLE"\n', } @patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call) @patch( "prowler.providers.aws.aws_provider.AwsProvider.generate_regional_clients", new=mock_generate_regional_clients, ) @mock_aws def test_get_repository_files_content_no_default_branch(self): codecommit = CodeCommit(set_mocked_aws_provider([AWS_REGION_EU_WEST_1])) repository = Repository( repository_id="empty-repo-id", name="empty-repo", arn=f"arn:{AWS_COMMERCIAL_PARTITION}:codecommit:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:empty-repo", region=AWS_REGION_EU_WEST_1, ) files = list(codecommit.get_repository_files_content(repository)) assert files == [] @patch( "botocore.client.BaseClient._make_api_call", new=mock_make_api_call_list_repositories_access_denied, ) @patch( "prowler.providers.aws.aws_provider.AwsProvider.generate_regional_clients", new=mock_generate_regional_clients, ) @mock_aws def test_list_repositories_access_denied_sets_none(self): """An AccessDenied error with no repositories collected yet sets repositories to None.""" codecommit = CodeCommit(set_mocked_aws_provider([AWS_REGION_EU_WEST_1])) assert codecommit.repositories is None @patch( "botocore.client.BaseClient._make_api_call", new=mock_make_api_call_list_repositories_client_error, ) @patch( "prowler.providers.aws.aws_provider.AwsProvider.generate_regional_clients", new=mock_generate_regional_clients, ) @mock_aws def test_list_repositories_other_client_error(self): """A non-AccessDenied ClientError is logged but does not set repositories to None.""" codecommit = CodeCommit(set_mocked_aws_provider([AWS_REGION_EU_WEST_1])) assert codecommit.repositories == {} def test_list_repositories_generic_exception(self): """A non-ClientError exception while listing repositories is caught and logged.""" codecommit = CodeCommit.__new__(CodeCommit) codecommit.repositories = {} codecommit.audited_partition = AWS_COMMERCIAL_PARTITION codecommit.audited_account = AWS_ACCOUNT_NUMBER regional_client = MagicMock() regional_client.region = AWS_REGION_EU_WEST_1 regional_client.get_paginator.side_effect = Exception("Generic error") codecommit._list_repositories(regional_client) assert codecommit.repositories == {} def test_list_repositories_reinitializes_after_none(self): """A region that succeeds after another region hit AccessDenied (leaving repositories as None) reinitializes it to a dict instead of crashing.""" codecommit = CodeCommit.__new__(CodeCommit) codecommit.repositories = None codecommit.audited_partition = AWS_COMMERCIAL_PARTITION codecommit.audited_account = AWS_ACCOUNT_NUMBER regional_client = MagicMock() regional_client.region = AWS_REGION_EU_WEST_1 paginator = MagicMock() paginator.paginate.return_value = [ { "repositories": [ {"repositoryName": repository_name, "repositoryId": repository_id} ] } ] regional_client.get_paginator.return_value = paginator codecommit._list_repositories(regional_client) assert isinstance(codecommit.repositories, dict) assert codecommit.repositories[repository_arn].name == repository_name def test_list_repositories_access_denied_keeps_existing_repositories(self): """An AccessDenied error hit after repositories were already collected (e.g. in another region) does not wipe out the ones already found.""" codecommit = CodeCommit.__new__(CodeCommit) codecommit.repositories = {} codecommit.audited_partition = AWS_COMMERCIAL_PARTITION codecommit.audited_account = AWS_ACCOUNT_NUMBER healthy_client = MagicMock() healthy_client.region = AWS_REGION_EU_WEST_1 healthy_paginator = MagicMock() healthy_paginator.paginate.return_value = [ { "repositories": [ {"repositoryName": repository_name, "repositoryId": repository_id} ] } ] healthy_client.get_paginator.return_value = healthy_paginator codecommit._list_repositories(healthy_client) assert repository_arn in codecommit.repositories failing_client = MagicMock() failing_client.region = "us-east-1" failing_paginator = MagicMock() failing_paginator.paginate.side_effect = ClientError( {"Error": {"Code": "AccessDenied", "Message": "Access Denied"}}, "ListRepositories", ) failing_client.get_paginator.return_value = failing_paginator codecommit._list_repositories(failing_client) assert codecommit.repositories is not None assert repository_arn in codecommit.repositories def test_get_repository_no_default_branch(self): """A repository with no default branch (e.g. a brand-new empty repo) is left without one, and GetBranch is never called.""" codecommit = CodeCommit.__new__(CodeCommit) regional_client = MagicMock() regional_client.get_repository.return_value = { "repositoryMetadata": { "repositoryId": repository_id, "repositoryName": repository_name, } } codecommit.regional_clients = {AWS_REGION_EU_WEST_1: regional_client} repository = Repository( repository_id=repository_id, name=repository_name, arn=repository_arn, region=AWS_REGION_EU_WEST_1, ) codecommit._get_repository(repository) assert repository.default_branch is None assert repository.default_branch_commit_id is None regional_client.get_branch.assert_not_called() @patch( "botocore.client.BaseClient._make_api_call", new=mock_make_api_call_repository_errors, ) @patch( "prowler.providers.aws.aws_provider.AwsProvider.generate_regional_clients", new=mock_generate_regional_clients, ) @mock_aws def test_get_repository_error_branches(self): """GetRepository/GetBranch errors are caught per-repository without affecting others.""" codecommit = CodeCommit(set_mocked_aws_provider([AWS_REGION_EU_WEST_1])) assert len(codecommit.repositories) == 4 def arn_for(name): return f"arn:{AWS_COMMERCIAL_PARTITION}:codecommit:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:{name}" not_exist = codecommit.repositories[arn_for("repo-not-exist")] assert not_exist.default_branch is None assert not_exist.default_branch_commit_id is None other_error = codecommit.repositories[arn_for("repo-other-error")] assert other_error.default_branch is None assert other_error.default_branch_commit_id is None branch_error = codecommit.repositories[arn_for("repo-branch-error")] assert branch_error.default_branch == default_branch assert branch_error.default_branch_commit_id is None healthy = codecommit.repositories[arn_for(repository_name)] assert healthy.default_branch == default_branch assert healthy.default_branch_commit_id == commit_id def test_get_repository_generic_exception(self): """A non-ClientError exception while getting repository metadata is caught and logged.""" codecommit = CodeCommit.__new__(CodeCommit) codecommit.regional_clients = {AWS_REGION_EU_WEST_1: MagicMock()} codecommit.regional_clients[AWS_REGION_EU_WEST_1].get_repository.side_effect = ( Exception("Generic error") ) repository = Repository( repository_id=repository_id, name=repository_name, arn=repository_arn, region=AWS_REGION_EU_WEST_1, ) codecommit._get_repository(repository) assert repository.default_branch is None @patch( "botocore.client.BaseClient._make_api_call", new=mock_make_api_call_tags_errors, ) @patch( "prowler.providers.aws.aws_provider.AwsProvider.generate_regional_clients", new=mock_generate_regional_clients, ) @mock_aws def test_list_tags_for_resource_error_branches(self): """ListTagsForResource errors are caught per-repository and tags stay empty.""" codecommit = CodeCommit(set_mocked_aws_provider([AWS_REGION_EU_WEST_1])) assert len(codecommit.repositories) == 2 for repository in codecommit.repositories.values(): assert repository.tags == {} def test_list_tags_for_resource_generic_exception(self): """A non-ClientError exception while listing tags is caught and logged.""" codecommit = CodeCommit.__new__(CodeCommit) codecommit.regional_clients = {AWS_REGION_EU_WEST_1: MagicMock()} codecommit.regional_clients[ AWS_REGION_EU_WEST_1 ].list_tags_for_resource.side_effect = Exception("Generic error") repository = Repository( repository_id=repository_id, name=repository_name, arn=repository_arn, region=AWS_REGION_EU_WEST_1, ) codecommit._list_tags_for_resource(repository) assert repository.tags == {} @patch( "botocore.client.BaseClient._make_api_call", new=mock_make_api_call_files_errors, ) @patch( "prowler.providers.aws.aws_provider.AwsProvider.generate_regional_clients", new=mock_generate_regional_clients, ) @mock_aws def test_get_repository_files_content_handles_errors(self): """Broken folders and blobs are yielded with None content so callers can report them as unscanned instead of silently passing.""" codecommit = CodeCommit(set_mocked_aws_provider([AWS_REGION_EU_WEST_1])) repository = codecommit.repositories[repository_arn] files = dict(codecommit.get_repository_files_content(repository)) assert files == { "README.md": b"# Test repository\n", "/src/secrets.py": b'AWS_ACCESS_KEY_ID = "AKIAIOSFODNN7EXAMPLE"\n', "/broken-folder": None, "/broken-folder-2": None, "bad-blob.txt": None, "error-blob.txt": None, }