Skip to content

Commit 0ad5bbf

Browse files
feat(github): Add GitHub check ensuring repository creation is limited (#8844)
Signed-off-by: Mauricio Harley <mauricioharley@gmail.com> Co-authored-by: Hugo Pereira Brito <101209179+HugoPBrito@users.noreply.github.com> Co-authored-by: HugoPBrito <hugopbrit@gmail.com>
1 parent 38f6096 commit 0ad5bbf

File tree

8 files changed

+500
-112
lines changed

8 files changed

+500
-112
lines changed

poetry.lock

Lines changed: 120 additions & 99 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

prowler/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
1313
- `cloudstorage_bucket_logging_enabled` check for GCP provider [(#9091)](https://github.com/prowler-cloud/prowler/pull/9091)
1414
- C5 compliance framework for Azure provider [(#9081)](https://github.com/prowler-cloud/prowler/pull/9081)
1515
- C5 compliance framework for the GCP provider [(#9097)](https://github.com/prowler-cloud/prowler/pull/9097)
16+
- `organization_repository_creation_limited` check for GitHub provider [(#8844)](https://github.com/prowler-cloud/prowler/pull/8844)
1617
- HIPAA compliance framework for the GCP provider [(#8955)](https://github.com/prowler-cloud/prowler/pull/8955)
1718
- Add multiple compliance improvements [(#9145)](https://github.com/prowler-cloud/prowler/pull/9145)
1819
- Added validation for invalid checks, services, and categories in `load_checks_to_execute` function [(#8971)](https://github.com/prowler-cloud/prowler/pull/8971)

prowler/compliance/github/cis_1.0_github.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -476,7 +476,9 @@
476476
{
477477
"Id": "1.2.2",
478478
"Description": "Limit the ability to create repositories to trusted users and teams.",
479-
"Checks": [],
479+
"Checks": [
480+
"organization_repository_creation_limited"
481+
],
480482
"Attributes": [
481483
{
482484
"Section": "1 Source Code",

prowler/providers/github/services/organization/organization_repository_creation_limited/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"Provider": "github",
3+
"CheckID": "organization_repository_creation_limited",
4+
"CheckTitle": "Ensure repository creation is limited to trusted organization members.",
5+
"CheckType": [],
6+
"ServiceName": "organization",
7+
"SubServiceName": "",
8+
"ResourceIdTemplate": "",
9+
"Severity": "high",
10+
"ResourceType": "GitHubOrganization",
11+
"Description": "Ensure that repository creation is restricted so that only trusted owners or specific teams can create new repositories within the organization.",
12+
"Risk": "Allowing all members to create repositories increases the likelihood of shadow repositories, data leakage, or malicious projects being introduced without oversight.",
13+
"RelatedUrl": "https://docs.github.com/en/organizations/managing-organization-settings/restricting-repository-creation-in-your-organization",
14+
"Remediation": {
15+
"Code": {
16+
"CLI": "",
17+
"NativeIaC": "",
18+
"Other": "",
19+
"Terraform": ""
20+
},
21+
"Recommendation": {
22+
"Text": "Disable repository creation for members or limit it to specific trusted teams by adjusting Member privileges in the organization's settings.",
23+
"Url": "https://docs.github.com/en/organizations/managing-organization-settings/restricting-repository-creation-in-your-organization"
24+
}
25+
},
26+
"Categories": [],
27+
"DependsOn": [],
28+
"RelatedTo": [],
29+
"Notes": ""
30+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
from typing import List
2+
3+
from prowler.lib.check.models import Check, CheckReportGithub
4+
from prowler.providers.github.services.organization.organization_client import (
5+
organization_client,
6+
)
7+
8+
9+
def _join_human_readable(items: List[str]) -> str:
10+
"""Return a simple human readable comma-separated list."""
11+
if not items:
12+
return ""
13+
if len(items) == 1:
14+
return items[0]
15+
return ", ".join(items[:-1]) + f" and {items[-1]}"
16+
17+
18+
class organization_repository_creation_limited(Check):
19+
"""Check if repository creation is limited to trusted organization members."""
20+
21+
def execute(self) -> List[CheckReportGithub]:
22+
findings = []
23+
for org in organization_client.organizations.values():
24+
repo_data = [
25+
getattr(org, "members_can_create_repositories", None),
26+
getattr(org, "members_can_create_public_repositories", None),
27+
getattr(org, "members_can_create_private_repositories", None),
28+
getattr(org, "members_can_create_internal_repositories", None),
29+
getattr(org, "members_allowed_repository_creation_type", None),
30+
]
31+
32+
if all(value is None for value in repo_data):
33+
continue
34+
35+
report = CheckReportGithub(metadata=self.metadata(), resource=org)
36+
37+
global_creation = getattr(org, "members_can_create_repositories", None)
38+
public_creation = getattr(
39+
org, "members_can_create_public_repositories", None
40+
)
41+
private_creation = getattr(
42+
org, "members_can_create_private_repositories", None
43+
)
44+
internal_creation = getattr(
45+
org, "members_can_create_internal_repositories", None
46+
)
47+
creation_type = getattr(
48+
org, "members_allowed_repository_creation_type", None
49+
)
50+
51+
type_flags = []
52+
enabled_types = []
53+
54+
if global_creation is not None:
55+
if global_creation:
56+
enabled_types.append("repositories of any type")
57+
else:
58+
type_flags.append(False)
59+
60+
visibility_flags = [
61+
(public_creation, "public repositories"),
62+
(private_creation, "private repositories"),
63+
(internal_creation, "internal repositories"),
64+
]
65+
66+
for flag, label in visibility_flags:
67+
if flag is not None:
68+
type_flags.append(flag)
69+
if flag:
70+
enabled_types.append(label)
71+
72+
if creation_type:
73+
normalized_type = creation_type.lower()
74+
if normalized_type == "none":
75+
type_flags.append(False)
76+
else:
77+
creation_messages = {
78+
"all": "repositories of any type",
79+
"public": "public repositories",
80+
"private": "private repositories",
81+
"internal": "internal repositories",
82+
"selected": "repositories for selected members or teams",
83+
}
84+
enabled_types.append(
85+
creation_messages.get(
86+
normalized_type, f"{creation_type} repositories"
87+
)
88+
)
89+
90+
restricted = bool(type_flags) and all(flag is False for flag in type_flags)
91+
92+
if restricted:
93+
report.status = "PASS"
94+
report.status_extended = f"Organization {org.name} has disabled repository creation for members."
95+
else:
96+
report.status = "FAIL"
97+
unique_enabled = list(dict.fromkeys(enabled_types))
98+
allowed_desc = _join_human_readable(unique_enabled)
99+
if allowed_desc:
100+
report.status_extended = f"Organization {org.name} allows members to create {allowed_desc}."
101+
else:
102+
report.status_extended = f"Organization {org.name} does not have enough data to confirm repository creation restrictions."
103+
104+
findings.append(report)
105+
106+
return findings

prowler/providers/github/services/organization/organization_service.py

Lines changed: 70 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,9 @@ def _list_organizations(self):
3838
org_names_to_check = set()
3939

4040
try:
41-
for client in getattr(self, "clients", []) or []:
42-
if getattr(self.provider, "organizations", None):
41+
clients = getattr(self, "clients", [])
42+
for client in clients:
43+
if self.provider.organizations:
4344
org_names_to_check.update(self.provider.organizations)
4445

4546
# If repositories are specified without organizations, don't perform organization checks
@@ -147,22 +148,74 @@ def _process_organization(self, org, organizations):
147148
logger.error(
148149
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
149150
)
151+
repo_creation_settings = {
152+
"members_can_create_repositories": None,
153+
"members_can_create_public_repositories": None,
154+
"members_can_create_private_repositories": None,
155+
"members_can_create_internal_repositories": None,
156+
"members_allowed_repository_creation_type": None,
157+
}
158+
159+
def _extract_flag(attribute: str, expected_type: type):
160+
"""Return attribute value if it matches expected type and is not a mock placeholder."""
161+
try:
162+
value = getattr(org, attribute, None)
163+
if hasattr(value, "_mock_parent"):
164+
return None
165+
if expected_type is bool and isinstance(value, bool):
166+
return value
167+
if expected_type is str and isinstance(value, str):
168+
return value
169+
return None
170+
except Exception as error:
171+
logger.error(
172+
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
173+
)
174+
return None
175+
176+
repo_creation_settings["members_can_create_repositories"] = _extract_flag(
177+
"members_can_create_repositories", bool
178+
)
179+
repo_creation_settings["members_can_create_public_repositories"] = (
180+
_extract_flag("members_can_create_public_repositories", bool)
181+
)
182+
repo_creation_settings["members_can_create_private_repositories"] = (
183+
_extract_flag("members_can_create_private_repositories", bool)
184+
)
185+
repo_creation_settings["members_can_create_internal_repositories"] = (
186+
_extract_flag("members_can_create_internal_repositories", bool)
187+
)
188+
repo_creation_settings["members_allowed_repository_creation_type"] = (
189+
_extract_flag("members_allowed_repository_creation_type", str)
190+
)
150191

151-
# Base permission (default repository permission for members)
152-
base_perm: Optional[str] = None
153-
try:
154-
base_perm = getattr(org, "default_repository_permission", None)
155-
except Exception as error:
156-
logger.error(
157-
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
158-
)
159-
base_perm = None
192+
base_permission_raw = _extract_flag("default_repository_permission", str)
193+
base_permission = (
194+
base_permission_raw.lower()
195+
if isinstance(base_permission_raw, str)
196+
else None
197+
)
160198

161199
organizations[org.id] = Org(
162200
id=org.id,
163201
name=org.login,
164202
mfa_required=require_mfa,
165-
base_permission=base_perm,
203+
members_can_create_repositories=repo_creation_settings[
204+
"members_can_create_repositories"
205+
],
206+
members_can_create_public_repositories=repo_creation_settings[
207+
"members_can_create_public_repositories"
208+
],
209+
members_can_create_private_repositories=repo_creation_settings[
210+
"members_can_create_private_repositories"
211+
],
212+
members_can_create_internal_repositories=repo_creation_settings[
213+
"members_can_create_internal_repositories"
214+
],
215+
members_allowed_repository_creation_type=repo_creation_settings[
216+
"members_allowed_repository_creation_type"
217+
],
218+
base_permission=base_permission,
166219
)
167220

168221

@@ -172,4 +225,9 @@ class Org(BaseModel):
172225
id: int
173226
name: str
174227
mfa_required: Optional[bool] = False
228+
members_can_create_repositories: Optional[bool] = None
229+
members_can_create_public_repositories: Optional[bool] = None
230+
members_can_create_private_repositories: Optional[bool] = None
231+
members_can_create_internal_repositories: Optional[bool] = None
232+
members_allowed_repository_creation_type: Optional[str] = None
175233
base_permission: Optional[str] = None

0 commit comments

Comments
 (0)