mirror of
https://github.com/prowler-cloud/prowler.git
synced 2025-12-19 05:17:47 +00:00
feat(github): add organization base repository permission strict check (CIS GitHub 1.3.8) (#8785)
Co-authored-by: akorshak-afg <alex.korshak@afg.org> Co-authored-by: Sergio Garcia <sergargar1@gmail.com> Co-authored-by: Andoni Alonso <14891798+andoniaf@users.noreply.github.com>
This commit is contained in:
6
.gitignore
vendored
6
.gitignore
vendored
@@ -39,6 +39,12 @@ secrets-*/
|
||||
# JUnit Reports
|
||||
junit-reports/
|
||||
|
||||
# Test and coverage artifacts
|
||||
*_coverage.xml
|
||||
pytest_*.xml
|
||||
.coverage
|
||||
htmlcov/
|
||||
|
||||
# VSCode files
|
||||
.vscode/
|
||||
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
|
||||
All notable changes to the **Prowler SDK** are documented in this file.
|
||||
|
||||
## [v5.14.0] (Prowler UNRELEASED)
|
||||
|
||||
### Added
|
||||
- GitHub provider check `organization_default_repository_permission_strict` [(#8785)](https://github.com/prowler-cloud/prowler/pull/8785)
|
||||
|
||||
## [v5.13.0] (Prowler v5.13.0)
|
||||
|
||||
### Added
|
||||
|
||||
@@ -753,7 +753,9 @@
|
||||
{
|
||||
"Id": "1.3.8",
|
||||
"Description": "Base permissions define the permission level automatically granted to all organization members. Define strict base access permissions for all of the repositories in the organization, including new ones.",
|
||||
"Checks": [],
|
||||
"Checks": [
|
||||
"organization_default_repository_permission_strict"
|
||||
],
|
||||
"Attributes": [
|
||||
{
|
||||
"Section": "1 Source Code",
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"Provider": "github",
|
||||
"CheckID": "organization_default_repository_permission_strict",
|
||||
"CheckTitle": "Ensure strict base repository permissions are set for the organization",
|
||||
"CheckType": [],
|
||||
"ServiceName": "organization",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "high",
|
||||
"ResourceType": "GitHubOrganization",
|
||||
"Description": "Ensure the organization's base repository permission for members is set to 'read' or 'none' to minimize risk.",
|
||||
"Risk": "If base repository permissions allow 'write' or 'admin' by default, organization members may unintentionally gain excessive privileges across repositories, increasing the risk of unauthorized changes or accidental modifications.",
|
||||
"RelatedUrl": "https://docs.github.com/en/organizations/managing-user-access-to-your-organizations-repositories/managing-repository-roles/setting-base-permissions-for-an-organization",
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "",
|
||||
"NativeIaC": "",
|
||||
"Other": "",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Set the organization's base repository permission to 'read' or 'none' for members, unless stricter requirements are needed.",
|
||||
"Url": "https://docs.github.com/en/organizations/managing-user-access-to-your-organizations-repositories/managing-repository-roles/setting-base-permissions-for-an-organization"
|
||||
}
|
||||
},
|
||||
"AdditionalURLs": [],
|
||||
"Categories": [],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
from typing import List
|
||||
|
||||
from prowler.lib.check.models import Check, CheckReportGithub
|
||||
from prowler.providers.github.services.organization.organization_client import (
|
||||
organization_client,
|
||||
)
|
||||
|
||||
|
||||
class organization_default_repository_permission_strict(Check):
|
||||
"""Check if an organization's base repository permission is set to a strict level.
|
||||
|
||||
PASS: base permission is "read" or "none"
|
||||
FAIL: base permission is "write" or "admin" (or any other non-strict value)
|
||||
"""
|
||||
|
||||
def execute(self) -> List[CheckReportGithub]:
|
||||
findings = []
|
||||
for org in organization_client.organizations.values():
|
||||
base_perm = getattr(org, "base_permission", None)
|
||||
if base_perm is None:
|
||||
# Unknown / no permission to read → skip producing a finding
|
||||
continue
|
||||
|
||||
p = str(base_perm).lower()
|
||||
report = CheckReportGithub(metadata=self.metadata(), resource=org)
|
||||
|
||||
if p in ("read", "none"):
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"Organization {org.name} base repository permission is '{p}', which is strict."
|
||||
else:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"Organization {org.name} base repository permission is '{p}', which is not strict."
|
||||
|
||||
findings.append(report)
|
||||
|
||||
return findings
|
||||
@@ -5,7 +5,7 @@ from pydantic.v1 import BaseModel
|
||||
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.providers.github.lib.service.service import GithubService
|
||||
from prowler.providers.github.models import GithubAppIdentityInfo, GithubIdentityInfo
|
||||
from prowler.providers.github.models import GithubAppIdentityInfo
|
||||
|
||||
|
||||
class Organization(GithubService):
|
||||
@@ -38,13 +38,15 @@ class Organization(GithubService):
|
||||
org_names_to_check = set()
|
||||
|
||||
try:
|
||||
for client in self.clients:
|
||||
if self.provider.organizations:
|
||||
for client in getattr(self, "clients", []) or []:
|
||||
if getattr(self.provider, "organizations", None):
|
||||
org_names_to_check.update(self.provider.organizations)
|
||||
|
||||
# If repositories are specified without organizations, don't perform organization checks
|
||||
# Only add repository owners to organization checks if organizations are also specified
|
||||
if self.provider.repositories and self.provider.organizations:
|
||||
if getattr(self.provider, "repositories", None) and getattr(
|
||||
self.provider, "organizations", None
|
||||
):
|
||||
for repo_name in self.provider.repositories:
|
||||
if "/" in repo_name:
|
||||
owner_name = repo_name.split("/")[0]
|
||||
@@ -111,18 +113,19 @@ class Organization(GithubService):
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
elif not self.provider.repositories:
|
||||
elif not getattr(self.provider, "repositories", None):
|
||||
# Default behavior: get all organizations the user is a member of
|
||||
# Only when no repositories are specified
|
||||
if isinstance(self.provider.identity, GithubIdentityInfo):
|
||||
if isinstance(self.provider.identity, GithubAppIdentityInfo):
|
||||
orgs = client.get_organizations()
|
||||
if getattr(orgs, "totalCount", 0) > 0:
|
||||
for org in orgs:
|
||||
self._process_organization(org, organizations)
|
||||
else:
|
||||
# Default (personal access/OAuth): use user organizations
|
||||
orgs = client.get_user().get_orgs()
|
||||
for org in orgs:
|
||||
self._process_organization(org, organizations)
|
||||
elif isinstance(self.provider.identity, GithubAppIdentityInfo):
|
||||
orgs = client.get_organizations()
|
||||
if orgs.totalCount > 0:
|
||||
for org in orgs:
|
||||
self._process_organization(org, organizations)
|
||||
|
||||
except github.RateLimitExceededException as error:
|
||||
logger.error(f"GitHub API rate limit exceeded: {error}")
|
||||
@@ -144,10 +147,22 @@ class Organization(GithubService):
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
# Base permission (default repository permission for members)
|
||||
base_perm: Optional[str] = None
|
||||
try:
|
||||
base_perm = getattr(org, "default_repository_permission", None)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
base_perm = None
|
||||
|
||||
organizations[org.id] = Org(
|
||||
id=org.id,
|
||||
name=org.login,
|
||||
mfa_required=require_mfa,
|
||||
base_permission=base_perm,
|
||||
)
|
||||
|
||||
|
||||
@@ -157,3 +172,4 @@ class Org(BaseModel):
|
||||
id: int
|
||||
name: str
|
||||
mfa_required: Optional[bool] = False
|
||||
base_permission: Optional[str] = None
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
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_repository_permission_strict:
|
||||
def test_no_organizations(self):
|
||||
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_repository_permission_strict.organization_default_repository_permission_strict.organization_client",
|
||||
new=organization_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.github.services.organization.organization_default_repository_permission_strict.organization_default_repository_permission_strict import (
|
||||
organization_default_repository_permission_strict,
|
||||
)
|
||||
|
||||
check = organization_default_repository_permission_strict()
|
||||
result = check.execute()
|
||||
assert len(result) == 0
|
||||
|
||||
def test_permission_read(self):
|
||||
organization_client = mock.MagicMock
|
||||
org_name = "test-organization"
|
||||
organization_client.organizations = {
|
||||
1: Org(
|
||||
id=1,
|
||||
name=org_name,
|
||||
base_permission="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_repository_permission_strict.organization_default_repository_permission_strict.organization_client",
|
||||
new=organization_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.github.services.organization.organization_default_repository_permission_strict.organization_default_repository_permission_strict import (
|
||||
organization_default_repository_permission_strict,
|
||||
)
|
||||
|
||||
check = organization_default_repository_permission_strict()
|
||||
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} base repository permission is 'read', which is strict."
|
||||
)
|
||||
|
||||
def test_permission_none(self):
|
||||
organization_client = mock.MagicMock
|
||||
org_name = "test-organization"
|
||||
organization_client.organizations = {
|
||||
1: Org(
|
||||
id=1,
|
||||
name=org_name,
|
||||
base_permission="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_repository_permission_strict.organization_default_repository_permission_strict.organization_client",
|
||||
new=organization_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.github.services.organization.organization_default_repository_permission_strict.organization_default_repository_permission_strict import (
|
||||
organization_default_repository_permission_strict,
|
||||
)
|
||||
|
||||
check = organization_default_repository_permission_strict()
|
||||
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} base repository permission is 'none', which is strict."
|
||||
)
|
||||
|
||||
def test_permission_write(self):
|
||||
organization_client = mock.MagicMock
|
||||
org_name = "test-organization"
|
||||
organization_client.organizations = {
|
||||
1: Org(
|
||||
id=1,
|
||||
name=org_name,
|
||||
base_permission="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_repository_permission_strict.organization_default_repository_permission_strict.organization_client",
|
||||
new=organization_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.github.services.organization.organization_default_repository_permission_strict.organization_default_repository_permission_strict import (
|
||||
organization_default_repository_permission_strict,
|
||||
)
|
||||
|
||||
check = organization_default_repository_permission_strict()
|
||||
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} base repository permission is 'write', which is not strict."
|
||||
)
|
||||
|
||||
def test_permission_admin(self):
|
||||
organization_client = mock.MagicMock
|
||||
org_name = "test-organization"
|
||||
organization_client.organizations = {
|
||||
1: Org(
|
||||
id=1,
|
||||
name=org_name,
|
||||
base_permission="admin",
|
||||
),
|
||||
}
|
||||
|
||||
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_repository_permission_strict.organization_default_repository_permission_strict.organization_client",
|
||||
new=organization_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.github.services.organization.organization_default_repository_permission_strict.organization_default_repository_permission_strict import (
|
||||
organization_default_repository_permission_strict,
|
||||
)
|
||||
|
||||
check = organization_default_repository_permission_strict()
|
||||
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} base repository permission is 'admin', which is not strict."
|
||||
)
|
||||
|
||||
def test_permission_unknown_none_skipped(self):
|
||||
organization_client = mock.MagicMock
|
||||
org_name = "test-organization"
|
||||
organization_client.organizations = {
|
||||
1: Org(
|
||||
id=1,
|
||||
name=org_name,
|
||||
base_permission=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_repository_permission_strict.organization_default_repository_permission_strict.organization_client",
|
||||
new=organization_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.github.services.organization.organization_default_repository_permission_strict.organization_default_repository_permission_strict import (
|
||||
organization_default_repository_permission_strict,
|
||||
)
|
||||
|
||||
check = organization_default_repository_permission_strict()
|
||||
result = check.execute()
|
||||
assert len(result) == 0
|
||||
@@ -45,11 +45,13 @@ class Test_Organization_Scoping:
|
||||
self.mock_org1.id = 1
|
||||
self.mock_org1.login = "test-org1"
|
||||
self.mock_org1.two_factor_requirement_enabled = True
|
||||
self.mock_org1.default_repository_permission = None
|
||||
|
||||
self.mock_org2 = MagicMock()
|
||||
self.mock_org2.id = 2
|
||||
self.mock_org2.login = "test-org2"
|
||||
self.mock_org2.two_factor_requirement_enabled = False
|
||||
self.mock_org2.default_repository_permission = None
|
||||
|
||||
self.mock_user = MagicMock()
|
||||
self.mock_user.id = 100
|
||||
@@ -175,6 +177,35 @@ class Test_Organization_Scoping:
|
||||
|
||||
assert len(orgs) == 0
|
||||
|
||||
def test_base_permission_extraction(self):
|
||||
"""Test that base_permission is populated from organization's default_repository_permission"""
|
||||
provider = set_mocked_github_provider()
|
||||
provider.repositories = []
|
||||
provider.organizations = ["test-org1"]
|
||||
|
||||
mock_client = MagicMock()
|
||||
# Organization with default_repository_permission set to "read"
|
||||
org_with_perm = MagicMock()
|
||||
org_with_perm.id = 1
|
||||
org_with_perm.login = "test-org1"
|
||||
org_with_perm.two_factor_requirement_enabled = True
|
||||
org_with_perm.default_repository_permission = "read"
|
||||
mock_client.get_organization.return_value = org_with_perm
|
||||
|
||||
with patch(
|
||||
"prowler.providers.github.services.organization.organization_service.GithubService.__init__"
|
||||
):
|
||||
organization_service = Organization(provider)
|
||||
organization_service.clients = [mock_client]
|
||||
organization_service.provider = provider
|
||||
|
||||
orgs = organization_service._list_organizations()
|
||||
|
||||
assert len(orgs) == 1
|
||||
assert 1 in orgs
|
||||
assert orgs[1].name == "test-org1"
|
||||
assert orgs[1].base_permission == "read"
|
||||
|
||||
def test_specific_organization_scoping(self):
|
||||
"""Test that only specified organizations are returned"""
|
||||
provider = set_mocked_github_provider()
|
||||
@@ -287,11 +318,13 @@ class Test_Organization_Scoping:
|
||||
mock_owner_org.id = 1
|
||||
mock_owner_org.login = "owner1"
|
||||
mock_owner_org.two_factor_requirement_enabled = True
|
||||
mock_owner_org.default_repository_permission = None
|
||||
|
||||
mock_specific_org = MagicMock()
|
||||
mock_specific_org.id = 2
|
||||
mock_specific_org.login = "specific-org"
|
||||
mock_specific_org.two_factor_requirement_enabled = False
|
||||
mock_specific_org.default_repository_permission = None
|
||||
|
||||
mock_client.get_organization.side_effect = [
|
||||
mock_owner_org,
|
||||
@@ -393,6 +426,7 @@ class Test_Organization_ErrorHandling:
|
||||
self.mock_org1.id = 1
|
||||
self.mock_org1.login = "test-org1"
|
||||
self.mock_org1.two_factor_requirement_enabled = True
|
||||
self.mock_org1.default_repository_permission = None
|
||||
|
||||
def test_github_api_error_handling(self):
|
||||
"""Test that GitHub API errors are handled properly"""
|
||||
|
||||
Reference in New Issue
Block a user