Skip to content

Commit d6b954e

Browse files
authored
fix(control-plane): honor enforced org security config (#39)
1 parent 117ae6c commit d6b954e

3 files changed

Lines changed: 153 additions & 2 deletions

File tree

apps/orchestrator/tests/test_github_control_plane.py

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ def _policy_payload() -> dict:
3535
"secret_scanning_push_protection": {"required": True, "mode": "api"},
3636
"secret_scanning_non_provider_patterns": {"required": True, "mode": "api"},
3737
"secret_scanning_validity_checks": {"required": True, "mode": "api"},
38+
"org_code_security_configuration": {
39+
"required": True,
40+
"mode": "api",
41+
"configuration_id": 240284,
42+
"required_repository_status": "enforced",
43+
},
3844
"dependabot_config": {"required": True, "mode": "repo_file", "path": ".github/dependabot.yml"},
3945
"codeql_workflow": {"required": True, "mode": "repo_file", "path": ".github/workflows/codeql.yml"},
4046
"codeql_config": {"required": True, "mode": "repo_file", "path": ".github/codeql/codeql-config.yml"},
@@ -48,6 +54,7 @@ def _impl(path: str) -> tuple[int, dict]:
4854
return (
4955
0,
5056
{
57+
"id": 123,
5158
"default_branch": "main",
5259
"security_and_analysis": {
5360
"secret_scanning": {"status": "enabled"},
@@ -71,6 +78,61 @@ def _impl(path: str) -> tuple[int, dict]:
7178
return 0, {}
7279
if path == "repos/example/repo/dependabot/alerts?per_page=1":
7380
return 0, []
81+
if path == "orgs/example/code-security/configurations/240284":
82+
return 0, {
83+
"id": 240284,
84+
"secret_scanning": "enabled",
85+
"secret_scanning_push_protection": "enabled",
86+
"secret_scanning_non_provider_patterns": "enabled",
87+
"secret_scanning_validity_checks": "enabled",
88+
}
89+
if path == "orgs/example/code-security/configurations/240284/repositories":
90+
return 0, [{"status": "enforced", "repository": {"id": 123}}]
91+
raise AssertionError(path)
92+
93+
return _impl
94+
95+
96+
def _fake_gh_json_without_org_enforcement(secret_non_provider: str, validity_checks: str):
97+
def _impl(path: str) -> tuple[int, dict]:
98+
if path == "repos/example/repo":
99+
return (
100+
0,
101+
{
102+
"id": 123,
103+
"default_branch": "main",
104+
"security_and_analysis": {
105+
"secret_scanning": {"status": "enabled"},
106+
"secret_scanning_push_protection": {"status": "enabled"},
107+
"secret_scanning_non_provider_patterns": {"status": secret_non_provider},
108+
"secret_scanning_validity_checks": {"status": validity_checks},
109+
},
110+
},
111+
)
112+
if path == "repos/example/repo/actions/permissions":
113+
return 0, {"enabled": True, "allowed_actions": "all", "sha_pinning_required": True}
114+
if path == "repos/example/repo/branches/main/protection":
115+
return 0, {"required_status_checks": {"contexts": ["Quick Feedback"]}}
116+
if path == "repos/example/repo/environments":
117+
return 0, {"environments": [{"name": "owner-approved-sensitive"}]}
118+
if path == "repos/example/repo/private-vulnerability-reporting":
119+
return 0, {"enabled": True}
120+
if path == "repos/example/repo/vulnerability-alerts":
121+
return 0, {}
122+
if path == "repos/example/repo/code-scanning/default-setup":
123+
return 0, {}
124+
if path == "repos/example/repo/dependabot/alerts?per_page=1":
125+
return 0, []
126+
if path == "orgs/example/code-security/configurations/240284":
127+
return 0, {
128+
"id": 240284,
129+
"secret_scanning": "enabled",
130+
"secret_scanning_push_protection": "enabled",
131+
"secret_scanning_non_provider_patterns": "enabled",
132+
"secret_scanning_validity_checks": "enabled",
133+
}
134+
if path == "orgs/example/code-security/configurations/240284/repositories":
135+
return 0, []
74136
raise AssertionError(path)
75137

76138
return _impl
@@ -83,7 +145,7 @@ def test_github_control_plane_requires_secret_scanning_subfeatures(tmp_path: Pat
83145
policy_path.write_text(json.dumps(_policy_payload(), ensure_ascii=False, indent=2), encoding="utf-8")
84146

85147
monkeypatch.setattr(module, "ROOT", tmp_path)
86-
monkeypatch.setattr(module, "_gh_json", _fake_gh_json("disabled", "disabled"))
148+
monkeypatch.setattr(module, "_gh_json", _fake_gh_json_without_org_enforcement("disabled", "disabled"))
87149
monkeypatch.setattr(module, "_repo_path_exists", lambda _path: True)
88150
monkeypatch.setattr(sys, "argv", ["check_github_control_plane.py", "--policy", str(policy_path), "--output", str(output_path)])
89151

@@ -110,3 +172,21 @@ def test_github_control_plane_accepts_enabled_secret_scanning_subfeatures(tmp_pa
110172
assert rc == 0
111173
assert report["errors"] == []
112174
assert report["security_and_analysis"]["secret_scanning_non_provider_patterns"]["status"] == "enabled"
175+
176+
177+
def test_github_control_plane_accepts_enforced_org_config_fallback(tmp_path: Path, monkeypatch) -> None:
178+
module = _load_module()
179+
policy_path = tmp_path / "policy.json"
180+
output_path = tmp_path / "report.json"
181+
policy_path.write_text(json.dumps(_policy_payload(), ensure_ascii=False, indent=2), encoding="utf-8")
182+
183+
monkeypatch.setattr(module, "ROOT", tmp_path)
184+
monkeypatch.setattr(module, "_gh_json", _fake_gh_json("disabled", "disabled"))
185+
monkeypatch.setattr(module, "_repo_path_exists", lambda _path: True)
186+
monkeypatch.setattr(sys, "argv", ["check_github_control_plane.py", "--policy", str(policy_path), "--output", str(output_path)])
187+
188+
rc = module.main()
189+
report = json.loads(output_path.read_text(encoding="utf-8"))
190+
assert rc == 0
191+
assert report["errors"] == []
192+
assert report["org_code_security_configuration"]["id"] == 240284

configs/github_control_plane_policy.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@
4242
"required": true,
4343
"mode": "api"
4444
},
45+
"org_code_security_configuration": {
46+
"required": true,
47+
"mode": "api",
48+
"configuration_id": 240284,
49+
"required_repository_status": "enforced"
50+
},
4551
"dependabot_config": {
4652
"required": true,
4753
"mode": "repo_file",

scripts/check_github_control_plane.py

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,41 @@ def _security_feature_status(repo_payload: dict, feature_name: str) -> str:
4242
return str(feature.get("status") or "").strip()
4343

4444

45+
def _org_configuration_id(platform_evidence: dict) -> str:
46+
cfg = platform_evidence.get("org_code_security_configuration")
47+
if not isinstance(cfg, dict):
48+
return ""
49+
return str(cfg.get("configuration_id") or "").strip()
50+
51+
52+
def _org_config_proves_feature(
53+
org_config_payload: dict,
54+
org_repo_payload: list[dict] | dict,
55+
*,
56+
repo_id: int | None,
57+
feature_name: str,
58+
required_repository_status: str,
59+
) -> bool:
60+
if repo_id is None:
61+
return False
62+
feature_value = str(org_config_payload.get(feature_name) or "").strip()
63+
if feature_value != "enabled":
64+
return False
65+
if not isinstance(org_repo_payload, list):
66+
return False
67+
for item in org_repo_payload:
68+
if not isinstance(item, dict):
69+
continue
70+
repo = item.get("repository")
71+
if not isinstance(repo, dict):
72+
continue
73+
if repo.get("id") != repo_id:
74+
continue
75+
status = str(item.get("status") or "").strip()
76+
return status == required_repository_status
77+
return False
78+
79+
4580
def main() -> int:
4681
parser = argparse.ArgumentParser(description="Validate live GitHub control-plane settings against repo policy.")
4782
parser.add_argument("--policy", default=str(DEFAULT_POLICY))
@@ -63,6 +98,11 @@ def main() -> int:
6398
vuln_alerts_code, vuln_alerts_payload = _gh_json(f"repos/{owner}/{repo}/vulnerability-alerts")
6499
codeql_code, codeql_payload = _gh_json(f"repos/{owner}/{repo}/code-scanning/default-setup")
65100
dependabot_code, dependabot_payload = _gh_json(f"repos/{owner}/{repo}/dependabot/alerts?per_page=1")
101+
org_config_id = ""
102+
org_config_code = 0
103+
org_config_payload: dict = {}
104+
org_config_repos_code = 0
105+
org_config_repos_payload: list[dict] | dict = []
66106

67107
if repo_code != 0:
68108
errors.append(f"gh api repo fetch failed: {repo_payload}")
@@ -105,13 +145,24 @@ def main() -> int:
105145

106146
platform_evidence = policy.get("platform_evidence") if isinstance(policy.get("platform_evidence"), dict) else {}
107147
if platform_evidence:
148+
org_config_id = _org_configuration_id(platform_evidence)
149+
if org_config_id:
150+
org_config_code, org_config_payload = _gh_json(f"orgs/{owner}/code-security/configurations/{org_config_id}")
151+
org_config_repos_code, org_config_repos_payload = _gh_json(
152+
f"orgs/{owner}/code-security/configurations/{org_config_id}/repositories"
153+
)
108154
pvr_required = bool((platform_evidence.get("private_vulnerability_reporting") or {}).get("required"))
109155
if pvr_required:
110156
if pvr_code != 0:
111157
errors.append(f"private vulnerability reporting not proven: {pvr_payload}")
112158
vulnerability_alerts_required = bool((platform_evidence.get("vulnerability_alerts") or {}).get("required"))
113159
if vulnerability_alerts_required and vuln_alerts_code != 0:
114160
errors.append(f"vulnerability alerts not proven: {vuln_alerts_payload}")
161+
repo_id = repo_payload.get("id") if isinstance(repo_payload, dict) else None
162+
required_repo_status = str(
163+
((platform_evidence.get("org_code_security_configuration") or {}) if isinstance(platform_evidence.get("org_code_security_configuration"), dict) else {}).get("required_repository_status")
164+
or "enforced"
165+
).strip()
115166
for feature_name in (
116167
"secret_scanning",
117168
"secret_scanning_push_protection",
@@ -121,10 +172,22 @@ def main() -> int:
121172
feature_rule = platform_evidence.get(feature_name) if isinstance(platform_evidence.get(feature_name), dict) else {}
122173
if feature_rule.get("required"):
123174
status = _security_feature_status(repo_payload, feature_name)
124-
if status != "enabled":
175+
if status != "enabled" and not _org_config_proves_feature(
176+
org_config_payload,
177+
org_config_repos_payload,
178+
repo_id=repo_id if isinstance(repo_id, int) else None,
179+
feature_name=feature_name,
180+
required_repository_status=required_repo_status,
181+
):
125182
errors.append(
126183
f"{feature_name} drift: actual={status or 'missing'!r} expected='enabled'"
127184
)
185+
org_cfg_rule = platform_evidence.get("org_code_security_configuration") if isinstance(platform_evidence.get("org_code_security_configuration"), dict) else {}
186+
if org_cfg_rule.get("required"):
187+
if org_config_code != 0:
188+
errors.append(f"org code-security configuration not proven: {org_config_payload}")
189+
elif org_config_repos_code != 0:
190+
errors.append(f"org code-security configuration repository binding not proven: {org_config_repos_payload}")
128191
dependabot_rule = platform_evidence.get("dependabot_config") if isinstance(platform_evidence.get("dependabot_config"), dict) else {}
129192
dependabot_path = str(dependabot_rule.get("path") or "").strip()
130193
if dependabot_path and not _repo_path_exists(dependabot_path):
@@ -161,6 +224,8 @@ def main() -> int:
161224
"private_vulnerability_reporting": pvr_payload if pvr_code == 0 else {"error": pvr_payload},
162225
"vulnerability_alerts": {"enabled": True} if vuln_alerts_code == 0 else {"error": vuln_alerts_payload},
163226
"security_and_analysis": repo_payload.get("security_and_analysis") if repo_code == 0 else {"error": repo_payload},
227+
"org_code_security_configuration": org_config_payload if org_config_id and org_config_code == 0 else {"error": org_config_payload} if org_config_id else {},
228+
"org_code_security_configuration_repositories": org_config_repos_payload if org_config_id and org_config_repos_code == 0 else {"error": org_config_repos_payload} if org_config_id else {},
164229
"codeql_default_setup": codeql_payload if codeql_code == 0 else {"error": codeql_payload},
165230
"dependabot_alerts": dependabot_payload if dependabot_code == 0 else {"error": dependabot_payload},
166231
"errors": errors,

0 commit comments

Comments
 (0)