Skip to content

Commit 9da0b0c

Browse files
kushpatel321Kush321andoniaf
authored
feat(github): add organization domain verification check (#10033)
Co-authored-by: Kush321 <kushp2018@gmail.com> Co-authored-by: Andoni A. <14891798+andoniaf@users.noreply.github.com>
1 parent 8c1da07 commit 9da0b0c

File tree

7 files changed

+212
-2
lines changed

7 files changed

+212
-2
lines changed

prowler/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
66

77
### 🚀 Added
88

9+
- `organization_verified_badge` check for GitHub provider [(#10033)](https://github.com/prowler-cloud/prowler/pull/10033)
910
- OpenStack provider `clouds_yaml_content` parameter for API integration [(#10003)](https://github.com/prowler-cloud/prowler/pull/10003)
1011
- `defender_safe_attachments_policy_enabled` check for M365 provider [(#9833)](https://github.com/prowler-cloud/prowler/pull/9833)
1112
- `defender_safelinks_policy_enabled` check for M365 provider [(#9832)](https://github.com/prowler-cloud/prowler/pull/9832)

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -778,7 +778,9 @@
778778
{
779779
"Id": "1.3.9",
780780
"Description": "Confirm the domains an organization owns with a \"Verified\" badge.",
781-
"Checks": [],
781+
"Checks": [
782+
"organization_verified_badge"
783+
],
782784
"Attributes": [
783785
{
784786
"Section": "1 Source Code",

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ def _list_organizations(self):
7676
id=user.id,
7777
name=user.login,
7878
mfa_required=None, # Users don't have MFA requirements like orgs
79+
is_verified=None,
7980
)
8081
logger.info(
8182
f"Added user '{user.login}' as organization for checks"
@@ -195,7 +196,7 @@ def _extract_flag(attribute: str, expected_type: type):
195196
if isinstance(base_permission_raw, str)
196197
else None
197198
)
198-
199+
is_verified = _extract_flag("is_verified", bool)
199200
organizations[org.id] = Org(
200201
id=org.id,
201202
name=org.login,
@@ -216,6 +217,7 @@ def _extract_flag(attribute: str, expected_type: type):
216217
"members_allowed_repository_creation_type"
217218
],
218219
base_permission=base_permission,
220+
is_verified=is_verified,
219221
)
220222

221223

@@ -231,3 +233,4 @@ class Org(BaseModel):
231233
members_can_create_internal_repositories: Optional[bool] = None
232234
members_allowed_repository_creation_type: Optional[str] = None
233235
base_permission: Optional[str] = None
236+
is_verified: Optional[bool] = None

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

Whitespace-only changes.
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"Provider": "github",
3+
"CheckID": "organization_verified_badge",
4+
"CheckTitle": "Ensure GitHub organization has a verified badge",
5+
"CheckType": [],
6+
"ServiceName": "organization",
7+
"SubServiceName": "",
8+
"ResourceIdTemplate": "",
9+
"Severity": "medium",
10+
"ResourceType": "GitHubOrganization",
11+
"ResourceGroup": "governance",
12+
"Description": "Checks whether a GitHub organization has a verified badge.",
13+
"Risk": "Unverified organizations may be easier to impersonate, increasing the risk of phishing or trust abuse.",
14+
"RelatedUrl": "",
15+
"AdditionalURLs": [
16+
"https://docs.github.com/en/organizations/managing-organization-settings/verifying-or-approving-a-domain-for-your-organization"
17+
],
18+
"Remediation": {
19+
"Code": {
20+
"CLI": "",
21+
"NativeIaC": "",
22+
"Other": "",
23+
"Terraform": ""
24+
},
25+
"Recommendation": {
26+
"Text": "Verify the organization identity by completing GitHub organization verification.",
27+
"Url": "https://hub.prowler.com/check/organization_verified_badge"
28+
}
29+
},
30+
"Categories": [
31+
"identity-access"
32+
],
33+
"DependsOn": [],
34+
"RelatedTo": [],
35+
"Notes": "This check uses the GitHub API field is_verified from organization metadata."
36+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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+
class organization_verified_badge(Check):
10+
"""Check if GitHub organizations are verified."""
11+
12+
def execute(self) -> List[CheckReportGithub]:
13+
findings: List[CheckReportGithub] = []
14+
15+
for org in organization_client.organizations.values():
16+
report = CheckReportGithub(metadata=self.metadata(), resource=org)
17+
18+
if org.is_verified:
19+
report.status = "PASS"
20+
report.status_extended = (
21+
f"Organization {org.name} is verified on GitHub."
22+
)
23+
else:
24+
report.status = "FAIL"
25+
report.status_extended = (
26+
f"Organization {org.name} is not verified on GitHub."
27+
)
28+
29+
findings.append(report)
30+
31+
return findings
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
from unittest import mock
2+
3+
from prowler.providers.github.services.organization.organization_service import Org
4+
from tests.providers.github.github_fixtures import set_mocked_github_provider
5+
6+
7+
class Test_organization_verified_badge:
8+
def test_no_organizations(self):
9+
organization_client = mock.MagicMock
10+
organization_client.organizations = {}
11+
12+
with (
13+
mock.patch(
14+
"prowler.providers.common.provider.Provider.get_global_provider",
15+
return_value=set_mocked_github_provider(),
16+
),
17+
mock.patch(
18+
"prowler.providers.github.services.organization.organization_verified_badge.organization_verified_badge.organization_client",
19+
new=organization_client,
20+
),
21+
):
22+
from prowler.providers.github.services.organization.organization_verified_badge.organization_verified_badge import (
23+
organization_verified_badge,
24+
)
25+
26+
check = organization_verified_badge()
27+
result = check.execute()
28+
assert len(result) == 0
29+
30+
def test_organization_is_verified_true_pass(self):
31+
organization_client = mock.MagicMock
32+
org_name = "test-organization"
33+
organization_client.organizations = {
34+
1: Org(
35+
id=1,
36+
name=org_name,
37+
is_verified=True,
38+
),
39+
}
40+
41+
with (
42+
mock.patch(
43+
"prowler.providers.common.provider.Provider.get_global_provider",
44+
return_value=set_mocked_github_provider(),
45+
),
46+
mock.patch(
47+
"prowler.providers.github.services.organization.organization_verified_badge.organization_verified_badge.organization_client",
48+
new=organization_client,
49+
),
50+
):
51+
from prowler.providers.github.services.organization.organization_verified_badge.organization_verified_badge import (
52+
organization_verified_badge,
53+
)
54+
55+
check = organization_verified_badge()
56+
result = check.execute()
57+
assert len(result) == 1
58+
assert result[0].resource_id == 1
59+
assert result[0].resource_name == org_name
60+
assert result[0].status == "PASS"
61+
assert (
62+
result[0].status_extended
63+
== f"Organization {org_name} is verified on GitHub."
64+
)
65+
66+
def test_organization_is_verified_false_fail(self):
67+
organization_client = mock.MagicMock
68+
org_name = "test-organization"
69+
organization_client.organizations = {
70+
1: Org(
71+
id=1,
72+
name=org_name,
73+
is_verified=False,
74+
),
75+
}
76+
77+
with (
78+
mock.patch(
79+
"prowler.providers.common.provider.Provider.get_global_provider",
80+
return_value=set_mocked_github_provider(),
81+
),
82+
mock.patch(
83+
"prowler.providers.github.services.organization.organization_verified_badge.organization_verified_badge.organization_client",
84+
new=organization_client,
85+
),
86+
):
87+
from prowler.providers.github.services.organization.organization_verified_badge.organization_verified_badge import (
88+
organization_verified_badge,
89+
)
90+
91+
check = organization_verified_badge()
92+
result = check.execute()
93+
assert len(result) == 1
94+
assert result[0].resource_id == 1
95+
assert result[0].resource_name == org_name
96+
assert result[0].status == "FAIL"
97+
assert (
98+
result[0].status_extended
99+
== f"Organization {org_name} is not verified on GitHub."
100+
)
101+
102+
def test_organization_is_verified_none_edge_case(self):
103+
organization_client = mock.MagicMock
104+
org_name = "test-organization"
105+
organization_client.organizations = {
106+
1: Org(
107+
id=1,
108+
name=org_name,
109+
is_verified=None,
110+
),
111+
}
112+
113+
with (
114+
mock.patch(
115+
"prowler.providers.common.provider.Provider.get_global_provider",
116+
return_value=set_mocked_github_provider(),
117+
),
118+
mock.patch(
119+
"prowler.providers.github.services.organization.organization_verified_badge.organization_verified_badge.organization_client",
120+
new=organization_client,
121+
),
122+
):
123+
from prowler.providers.github.services.organization.organization_verified_badge.organization_verified_badge import (
124+
organization_verified_badge,
125+
)
126+
127+
check = organization_verified_badge()
128+
result = check.execute()
129+
assert len(result) == 1
130+
assert result[0].resource_id == 1
131+
assert result[0].resource_name == org_name
132+
# Treat none like not verified (false)
133+
assert result[0].status == "FAIL"
134+
assert (
135+
result[0].status_extended
136+
== f"Organization {org_name} is not verified on GitHub."
137+
)

0 commit comments

Comments
 (0)