feat(github): add organization_default_workflow_permissions_read_only check (#12122)

Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
This commit is contained in:
Chethas Dileep
2026-08-26 12:21:49 +02:00
committed by GitHub
co-authored by Daniel Barranquero
parent 95642fb220
commit 9dc53ffc60
8 changed files with 360 additions and 9 deletions
@@ -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)