mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-04 02:04:06 +00:00
feat(github): add organization_default_workflow_permissions_read_only check (#12122)
Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
This commit is contained in:
co-authored by
Daniel Barranquero
parent
95642fb220
commit
9dc53ffc60
+132
@@ -0,0 +1,132 @@
|
||||
from unittest import mock
|
||||
|
||||
from prowler.providers.github.services.organization.organization_service import Org
|
||||
from tests.providers.github.github_fixtures import set_mocked_github_provider
|
||||
|
||||
|
||||
class Test_organization_default_workflow_permissions_read_only:
|
||||
def test_no_organizations(self):
|
||||
"""Test that no findings are reported when there are no organizations"""
|
||||
organization_client = mock.MagicMock
|
||||
organization_client.organizations = {}
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_github_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.github.services.organization.organization_default_workflow_permissions_read_only.organization_default_workflow_permissions_read_only.organization_client",
|
||||
new=organization_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.github.services.organization.organization_default_workflow_permissions_read_only.organization_default_workflow_permissions_read_only import (
|
||||
organization_default_workflow_permissions_read_only,
|
||||
)
|
||||
|
||||
check = organization_default_workflow_permissions_read_only()
|
||||
result = check.execute()
|
||||
assert len(result) == 0
|
||||
|
||||
def test_organization_default_workflow_permissions_unknown(self):
|
||||
"""Test that no finding is reported when the setting could not be read"""
|
||||
organization_client = mock.MagicMock
|
||||
organization_client.organizations = {
|
||||
1: Org(
|
||||
id=1,
|
||||
name="test-organization",
|
||||
default_workflow_permissions=None,
|
||||
),
|
||||
}
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_github_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.github.services.organization.organization_default_workflow_permissions_read_only.organization_default_workflow_permissions_read_only.organization_client",
|
||||
new=organization_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.github.services.organization.organization_default_workflow_permissions_read_only.organization_default_workflow_permissions_read_only import (
|
||||
organization_default_workflow_permissions_read_only,
|
||||
)
|
||||
|
||||
check = organization_default_workflow_permissions_read_only()
|
||||
result = check.execute()
|
||||
assert len(result) == 0
|
||||
|
||||
def test_organization_default_workflow_permissions_write(self):
|
||||
"""Test that a write-capable default GITHUB_TOKEN fails the check"""
|
||||
organization_client = mock.MagicMock
|
||||
org_name = "test-organization"
|
||||
organization_client.organizations = {
|
||||
1: Org(
|
||||
id=1,
|
||||
name=org_name,
|
||||
default_workflow_permissions="write",
|
||||
),
|
||||
}
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_github_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.github.services.organization.organization_default_workflow_permissions_read_only.organization_default_workflow_permissions_read_only.organization_client",
|
||||
new=organization_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.github.services.organization.organization_default_workflow_permissions_read_only.organization_default_workflow_permissions_read_only import (
|
||||
organization_default_workflow_permissions_read_only,
|
||||
)
|
||||
|
||||
check = organization_default_workflow_permissions_read_only()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].resource_id == 1
|
||||
assert result[0].resource_name == org_name
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Organization {org_name} grants workflows a default GITHUB_TOKEN with write permissions."
|
||||
)
|
||||
|
||||
def test_organization_default_workflow_permissions_read(self):
|
||||
"""Test that a read-only default GITHUB_TOKEN passes the check"""
|
||||
organization_client = mock.MagicMock
|
||||
org_name = "test-organization"
|
||||
organization_client.organizations = {
|
||||
1: Org(
|
||||
id=1,
|
||||
name=org_name,
|
||||
default_workflow_permissions="read",
|
||||
),
|
||||
}
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_github_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.github.services.organization.organization_default_workflow_permissions_read_only.organization_default_workflow_permissions_read_only.organization_client",
|
||||
new=organization_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.github.services.organization.organization_default_workflow_permissions_read_only.organization_default_workflow_permissions_read_only import (
|
||||
organization_default_workflow_permissions_read_only,
|
||||
)
|
||||
|
||||
check = organization_default_workflow_permissions_read_only()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].resource_id == 1
|
||||
assert result[0].resource_name == org_name
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Organization {org_name} grants workflows a read-only default GITHUB_TOKEN."
|
||||
)
|
||||
@@ -1,6 +1,7 @@
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from github import GithubException, RateLimitExceededException
|
||||
from pytest import raises
|
||||
|
||||
from prowler.providers.github.services.organization.organization_service import (
|
||||
Org,
|
||||
@@ -464,7 +465,7 @@ class Test_Organization_ErrorHandling:
|
||||
assert any("Access denied" in msg for msg in log_messages)
|
||||
|
||||
def test_rate_limit_error_handling(self):
|
||||
"""Test that rate limit errors are logged appropriately"""
|
||||
"""Test that rate limit errors in the scoped-organization path are propagated"""
|
||||
provider = set_mocked_github_provider()
|
||||
provider.repositories = []
|
||||
provider.organizations = ["test-org1"]
|
||||
@@ -484,11 +485,95 @@ class Test_Organization_ErrorHandling:
|
||||
with patch(
|
||||
"prowler.providers.github.services.organization.organization_service.logger"
|
||||
) as mock_logger:
|
||||
# Rate limit errors should be caught and logged at the outer level
|
||||
orgs = organization_service._list_organizations()
|
||||
# Rate limit errors are logged and propagated instead of silently
|
||||
# dropping the scoped organization
|
||||
with raises(RateLimitExceededException):
|
||||
organization_service._list_organizations()
|
||||
|
||||
# Should be empty due to rate limit error
|
||||
assert len(orgs) == 0
|
||||
# Should log rate limit error
|
||||
mock_logger.error.assert_called()
|
||||
assert "Rate limit exceeded" in str(mock_logger.error.call_args)
|
||||
mock_client.get_user.assert_not_called()
|
||||
|
||||
|
||||
class Test_Organization_Default_Workflow_Permissions:
|
||||
def _get_service(self):
|
||||
"""Build an Organization service without contacting GitHub"""
|
||||
with patch(
|
||||
"prowler.providers.github.services.organization.organization_service.GithubService.__init__"
|
||||
):
|
||||
return Organization(set_mocked_github_provider())
|
||||
|
||||
def test_default_workflow_permissions_read(self):
|
||||
"""Test that the default workflow permissions value is read from the API response"""
|
||||
organization_service = self._get_service()
|
||||
mock_org = MagicMock()
|
||||
mock_org.login = "test-org"
|
||||
mock_org._requester.requestJsonAndCheck.return_value = (
|
||||
{},
|
||||
{"default_workflow_permissions": "read"},
|
||||
)
|
||||
|
||||
assert (
|
||||
organization_service._get_default_workflow_permissions(mock_org) == "read"
|
||||
), "The organization default GITHUB_TOKEN permissions should be read from the Actions permissions endpoint"
|
||||
mock_org._requester.requestJsonAndCheck.assert_called_once_with(
|
||||
"GET",
|
||||
"/orgs/test-org/actions/permissions/workflow",
|
||||
headers={
|
||||
"Accept": "application/vnd.github+json",
|
||||
"X-GitHub-Api-Version": "2022-11-28",
|
||||
},
|
||||
)
|
||||
|
||||
def test_default_workflow_permissions_missing_field(self):
|
||||
"""Test that a response without the permissions field yields no value"""
|
||||
organization_service = self._get_service()
|
||||
mock_org = MagicMock()
|
||||
mock_org.login = "test-org"
|
||||
mock_org._requester.requestJsonAndCheck.return_value = ({}, {})
|
||||
|
||||
assert (
|
||||
organization_service._get_default_workflow_permissions(mock_org) is None
|
||||
), "An unexpected response payload should not be reported as a permissions value"
|
||||
|
||||
def test_default_workflow_permissions_not_available(self):
|
||||
"""Test that accounts without Actions permissions settings yield no value"""
|
||||
organization_service = self._get_service()
|
||||
mock_org = MagicMock()
|
||||
mock_org.login = "test-user"
|
||||
mock_org._requester.requestJsonAndCheck.side_effect = GithubException(
|
||||
404, "Not Found", None
|
||||
)
|
||||
|
||||
assert (
|
||||
organization_service._get_default_workflow_permissions(mock_org) is None
|
||||
), "Accounts without Actions permissions settings should not produce a value"
|
||||
|
||||
def test_default_workflow_permissions_access_denied(self):
|
||||
"""Test that a token without organization administration access yields no value"""
|
||||
organization_service = self._get_service()
|
||||
mock_org = MagicMock()
|
||||
mock_org.login = "test-org"
|
||||
mock_org._requester.requestJsonAndCheck.side_effect = GithubException(
|
||||
403, "Forbidden", None
|
||||
)
|
||||
|
||||
with patch(
|
||||
"prowler.providers.github.services.organization.organization_service.logger"
|
||||
) as mock_logger:
|
||||
assert (
|
||||
organization_service._get_default_workflow_permissions(mock_org) is None
|
||||
), "Insufficient permissions should not produce a value"
|
||||
mock_logger.warning.assert_called()
|
||||
|
||||
def test_default_workflow_permissions_rate_limit(self):
|
||||
"""Test that rate limit errors are propagated instead of being reported as unavailable"""
|
||||
organization_service = self._get_service()
|
||||
mock_org = MagicMock()
|
||||
mock_org.login = "test-org"
|
||||
mock_org._requester.requestJsonAndCheck.side_effect = (
|
||||
RateLimitExceededException(403, "Rate limit exceeded", None)
|
||||
)
|
||||
|
||||
with raises(RateLimitExceededException):
|
||||
organization_service._get_default_workflow_permissions(mock_org)
|
||||
|
||||
Reference in New Issue
Block a user