diff --git a/example-data/testorg-unremediated.json b/example-data/testorg-unremediated.json index bd7926c..7cd5cbf 100644 --- a/example-data/testorg-unremediated.json +++ b/example-data/testorg-unremediated.json @@ -59,5 +59,32 @@ "secret_scanning_push_protection_custom_link_enabled": false, "secret_scanning_push_protection_custom_link": null, "secret_scanning_validity_checks_enabled": false - } + }, + "owners": [ + {"login": "admin-user-1", "id": 1001}, + {"login": "admin-user-2", "id": 1002}, + {"login": "admin-user-3", "id": 1003}, + {"login": "admin-user-4", "id": 1004}, + {"login": "admin-user-5", "id": 1005}, + {"login": "admin-user-6", "id": 1006}, + {"login": "admin-user-7", "id": 1007} + ], + "teams": [], + "sso": { + "enabled": false, + "enforced": false, + "sso_url": "", + "idp_issuer": "" + }, + "default_security_configs": [ + { + "default_for_new_repos": "all", + "configuration": { + "name": "Legacy Security Profile", + "secret_scanning": "disabled", + "dependabot_alerts": "not_set" + } + } + ], + "ip_allow_list": [] } diff --git a/example-data/testorg.json b/example-data/testorg.json index 946e674..caae066 100644 --- a/example-data/testorg.json +++ b/example-data/testorg.json @@ -31,12 +31,12 @@ "collaborators": 0, "billing_email": "test@example.com", "default_repository_permission": "read", - "members_can_create_repositories": true, + "members_can_create_repositories": false, "two_factor_requirement_enabled": true, - "members_allowed_repository_creation_type": "all", + "members_allowed_repository_creation_type": "none", "members_can_create_public_repositories": false, - "members_can_create_private_repositories": true, - "members_can_create_internal_repositories": true, + "members_can_create_private_repositories": false, + "members_can_create_internal_repositories": false, "members_can_create_pages": false, "members_can_fork_private_repositories": false, "web_commit_signoff_required": true, @@ -59,5 +59,33 @@ "secret_scanning_push_protection_custom_link_enabled": true, "secret_scanning_push_protection_custom_link": null, "secret_scanning_validity_checks_enabled": true - } + }, + "owners": [ + {"login": "admin-user-1", "id": 1001}, + {"login": "admin-user-2", "id": 1002} + ], + "teams": [ + {"name": "developers", "privacy": "closed", "description": "Application development team"}, + {"name": "security", "privacy": "closed", "description": "Security operations team"} + ], + "sso": { + "enabled": true, + "enforced": true, + "sso_url": "https://sso.example.com/saml/github", + "idp_issuer": "https://sso.example.com" + }, + "default_security_configs": [ + { + "default_for_new_repos": "all", + "configuration": { + "name": "Baseline Security Profile", + "secret_scanning": "enabled", + "dependabot_alerts": "enabled" + } + } + ], + "ip_allow_list": [ + {"allow_list_value": "203.0.113.0/24", "is_active": true, "name": "Office Network"}, + {"allow_list_value": "198.51.100.0/24", "is_active": true, "name": "VPN"} + ] } diff --git a/policies/gh_org_default_repo_permission.rego b/policies/gh_org_default_repo_permission.rego new file mode 100644 index 0000000..fe11b24 --- /dev/null +++ b/policies/gh_org_default_repo_permission.rego @@ -0,0 +1,57 @@ +package compliance_framework.default_repo_permission + +risk_templates := [ + { + "name": "Default repository permission is too permissive", + "title": "Overly Permissive Default Repository Access Grants Excessive Privileges to All Members", + "statement": "The default repository permission setting determines the base access level automatically granted to every organization member on all repositories. Setting this to 'write' or 'admin' means that all organization members, including newly onboarded employees and contractors, receive write or administrative access to every repository by default. This violates the principle of least privilege and can lead to unauthorized modifications, accidental data loss, or privilege escalation if any member account is compromised. The default should be 'read' or 'none', with elevated access granted explicitly via team membership.", + "likelihood_hint": "moderate", + "impact_hint": "high", + "violation_ids": ["default_permission_too_permissive"], + "threat_refs": [ + { + "system": "https://cwe.mitre.org", + "external_id": "CWE-269", + "title": "Improper Privilege Management", + "url": "https://cwe.mitre.org/data/definitions/269.html" + }, + { + "system": "https://cwe.mitre.org", + "external_id": "CWE-284", + "title": "Improper Access Control", + "url": "https://cwe.mitre.org/data/definitions/284.html" + }, + { + "system": "https://cwe.mitre.org", + "external_id": "CWE-732", + "title": "Incorrect Permission Assignment for Critical Resource", + "url": "https://cwe.mitre.org/data/definitions/732.html" + } + ], + "remediation": { + "title": "Set the default repository permission to 'read' or 'none'", + "description": "Configure the organization's default repository permission to 'read' or 'none'. Grant write and admin access explicitly via team membership to specific repositories, following the principle of least privilege.", + "tasks": [ + { "title": "Navigate to Organization Settings > Member privileges > Base permissions" }, + { "title": "Change the base permission to 'Read' or 'No permission'" }, + { "title": "Review all repositories to ensure teams have explicit access grants where write access is required" }, + { "title": "Communicate the change to all members and update onboarding documentation" }, + { "title": "Audit existing repositories for any direct-user write grants that should be team-based" } + ] + } + } +] + +_settings := object.get(input, "settings", {}) + +_default_repository_permission := object.get(_settings, "default_repository_permission", "") + +_allowed_permissions := {"read", "none"} + +violation[{"id": "default_permission_too_permissive"}] if { + not _allowed_permissions[_default_repository_permission] +} + +title := "Default repository permission is set to 'read' or 'none'" +description := "The organization's default repository permission must not grant write or admin access to all members by default. Elevated access should be granted explicitly via team membership to follow the principle of least privilege." +remarks := "More information: https://docs.github.com/en/organizations/managing-user-access-to-your-organizations-repositories/managing-repository-roles/setting-base-permissions-for-an-organization" diff --git a/policies/gh_org_default_repo_permission_test.rego b/policies/gh_org_default_repo_permission_test.rego new file mode 100644 index 0000000..74712b4 --- /dev/null +++ b/policies/gh_org_default_repo_permission_test.rego @@ -0,0 +1,37 @@ +package compliance_framework.default_repo_permission + +test_default_permission_read if { + count(violation) == 0 with input as { + "settings": { + "default_repository_permission": "read" + } + } +} + +test_default_permission_none if { + count(violation) == 0 with input as { + "settings": { + "default_repository_permission": "none" + } + } +} + +test_default_permission_write if { + count(violation) > 0 with input as { + "settings": { + "default_repository_permission": "write" + } + } +} + +test_default_permission_admin if { + count(violation) > 0 with input as { + "settings": { + "default_repository_permission": "admin" + } + } +} + +test_default_permission_missing if { + count(violation) > 0 with input as {} +} diff --git a/policies/gh_org_ip_allowlist_enabled.rego b/policies/gh_org_ip_allowlist_enabled.rego new file mode 100644 index 0000000..214c79e --- /dev/null +++ b/policies/gh_org_ip_allowlist_enabled.rego @@ -0,0 +1,53 @@ +package compliance_framework.ip_allowlist_enabled + +risk_templates := [ + { + "name": "No IP allow-list configured for the organization", + "title": "Absence of IP Allow-List Exposes GitHub Resources to Access from Untrusted Networks", + "statement": "Without an IP allow-list, the GitHub organization's resources (repositories, API, settings) are accessible from any IP address on the internet, subject only to authentication. This means that even valid credentials used from untrusted networks (e.g., compromised endpoints, attacker infrastructure) can interact with the organization's assets. Configuring an IP allow-list restricts access to approved network ranges, adding a network-layer control that limits the blast radius of credential compromise.", + "likelihood_hint": "moderate", + "impact_hint": "high", + "violation_ids": ["ip_allowlist_not_configured"], + "threat_refs": [ + { + "system": "https://cwe.mitre.org", + "external_id": "CWE-284", + "title": "Improper Access Control", + "url": "https://cwe.mitre.org/data/definitions/284.html" + }, + { + "system": "https://cwe.mitre.org", + "external_id": "CWE-923", + "title": "Improper Restriction of Communication Channel to Intended Endpoints", + "url": "https://cwe.mitre.org/data/definitions/923.html" + } + ], + "remediation": { + "title": "Configure an IP allow-list for the GitHub organization", + "description": "Enable the IP allow-list feature for the organization and add the approved IP ranges from which members are permitted to access GitHub. This restricts access to known, trusted networks and reduces the risk of credential-based attacks from untrusted locations.", + "tasks": [ + { "title": "Navigate to Organization Settings > Security > IP allow list" }, + { "title": "Enable 'IP allow list'" }, + { "title": "Add approved IP ranges for corporate offices, VPNs, and CI/CD infrastructure" }, + { "title": "Test that members can still access GitHub from approved networks before fully enforcing" }, + { "title": "Document the process for requesting additions to the IP allow-list" }, + { "title": "Schedule periodic review of the IP allow-list to remove stale entries" } + ] + } + } +] + +_ip_allow_list := object.get(input, "ip_allow_list", []) + +_has_active_entry if { + some entry in _ip_allow_list + entry.is_active == true +} + +violation[{"id": "ip_allowlist_not_configured"}] if { + not _has_active_entry +} + +title := "Organization has an active IP allow-list configured" +description := "The GitHub organization must have at least one active IP allow-list entry to restrict access to approved network ranges and reduce the risk of access from untrusted locations." +remarks := "More information: https://docs.github.com/en/organizations/keeping-your-organization-secure/managing-security-settings-for-your-organization/managing-allowed-ip-addresses-for-your-organization" diff --git a/policies/gh_org_ip_allowlist_enabled_test.rego b/policies/gh_org_ip_allowlist_enabled_test.rego new file mode 100644 index 0000000..b5ce7a4 --- /dev/null +++ b/policies/gh_org_ip_allowlist_enabled_test.rego @@ -0,0 +1,29 @@ +package compliance_framework.ip_allowlist_enabled + +test_ip_allowlist_configured if { + count(violation) == 0 with input as { + "ip_allow_list": [ + {"allow_list_value": "203.0.113.0/24", "is_active": true, "name": "Office"}, + {"allow_list_value": "198.51.100.0/24", "is_active": false, "name": "Old VPN"} + ] + } +} + +test_ip_allowlist_all_inactive if { + count(violation) > 0 with input as { + "ip_allow_list": [ + {"allow_list_value": "203.0.113.0/24", "is_active": false, "name": "Disabled"}, + {"allow_list_value": "198.51.100.0/24", "is_active": false, "name": "Also Disabled"} + ] + } +} + +test_ip_allowlist_empty if { + count(violation) > 0 with input as { + "ip_allow_list": [] + } +} + +test_ip_allowlist_missing if { + count(violation) > 0 with input as {} +} diff --git a/policies/gh_org_members_can_create_repos.rego b/policies/gh_org_members_can_create_repos.rego new file mode 100644 index 0000000..90d4a6f --- /dev/null +++ b/policies/gh_org_members_can_create_repos.rego @@ -0,0 +1,49 @@ +package compliance_framework.members_can_create_repos + +risk_templates := [ + { + "name": "Organization members can create repositories without restriction", + "title": "Unrestricted Repository Creation Undermines Access Governance", + "statement": "When all organization members are permitted to create repositories, the organization loses control over its asset inventory. Members may inadvertently expose internal code via public repositories, create repositories that bypass security baselines, or accumulate ungoverned codebases. Restricting repository creation to administrators ensures that new repositories are intentional, properly configured, and subject to security review before use.", + "likelihood_hint": "moderate", + "impact_hint": "high", + "violation_ids": ["members_can_create_repos"], + "threat_refs": [ + { + "system": "https://cwe.mitre.org", + "external_id": "CWE-284", + "title": "Improper Access Control", + "url": "https://cwe.mitre.org/data/definitions/284.html" + }, + { + "system": "https://cwe.mitre.org", + "external_id": "CWE-200", + "title": "Exposure of Sensitive Information to an Unauthorized Actor", + "url": "https://cwe.mitre.org/data/definitions/200.html" + } + ], + "remediation": { + "title": "Restrict repository creation to organization administrators", + "description": "Disable the ability for regular organization members to create new repositories. Only administrators should be permitted to create repositories, ensuring each new repository is intentionally provisioned and subject to organizational security baselines.", + "tasks": [ + { "title": "Navigate to Organization Settings > Member privileges" }, + { "title": "Review the Repository creation section for member repository creation settings" }, + { "title": "Disable 'Allow members to create repositories' or restrict repository creation to administrators" }, + { "title": "Review and archive any repositories created without administrative approval" }, + { "title": "Document a repository provisioning process that routes requests through an administrator" } + ] + } + } +] + +_settings := object.get(input, "settings", {}) + +_members_can_create_repositories := object.get(_settings, "members_can_create_repositories", true) + +violation[{"id": "members_can_create_repos"}] if { + _members_can_create_repositories +} + +title := "Organization members cannot create repositories" +description := "Repository creation should be restricted to administrators to maintain control over the organization's code asset inventory and prevent ungoverned or accidentally public repositories." +remarks := "More information: https://docs.github.com/en/organizations/managing-organization-settings/restricting-repository-creation-in-your-organization" diff --git a/policies/gh_org_members_can_create_repos_test.rego b/policies/gh_org_members_can_create_repos_test.rego new file mode 100644 index 0000000..34bf2c9 --- /dev/null +++ b/policies/gh_org_members_can_create_repos_test.rego @@ -0,0 +1,21 @@ +package compliance_framework.members_can_create_repos + +test_members_cannot_create_repos if { + count(violation) == 0 with input as { + "settings": { + "members_can_create_repositories": false + } + } +} + +test_members_can_create_repos if { + count(violation) > 0 with input as { + "settings": { + "members_can_create_repositories": true + } + } +} + +test_members_create_repos_missing if { + count(violation) > 0 with input as {} +} diff --git a/policies/gh_org_owner_count.rego b/policies/gh_org_owner_count.rego new file mode 100644 index 0000000..b645f59 --- /dev/null +++ b/policies/gh_org_owner_count.rego @@ -0,0 +1,83 @@ +package compliance_framework.owner_count + +risk_templates := [ + { + "name": "Excessive number of organization owners", + "title": "Too Many Organization Owners Increases Blast Radius of Privileged Account Compromise", + "statement": "Organization owners in GitHub hold the highest level of privilege: they can modify security settings, manage all members and teams, access all repositories, and permanently delete the organization. Granting owner access to more than 5 individuals significantly increases the attack surface for privilege abuse, insider threats, and account compromise scenarios. Limiting ownership to a small, well-controlled set ensures that elevated access is deliberately granted and periodically reviewed.", + "likelihood_hint": "moderate", + "impact_hint": "high", + "violation_ids": ["too_many_owners"], + "threat_refs": [ + { + "system": "https://cwe.mitre.org", + "external_id": "CWE-269", + "title": "Improper Privilege Management", + "url": "https://cwe.mitre.org/data/definitions/269.html" + }, + { + "system": "https://cwe.mitre.org", + "external_id": "CWE-284", + "title": "Improper Access Control", + "url": "https://cwe.mitre.org/data/definitions/284.html" + } + ], + "remediation": { + "title": "Reduce organization owner count to 5 or fewer", + "description": "Review the list of organization owners and remove owner access from any accounts that do not require it. Prefer using team-based admin roles for day-to-day administrative tasks, reserving full organization ownership for a minimal set of accountable individuals.", + "tasks": [ + { "title": "Navigate to Organization Settings > People > Owners" }, + { "title": "Review the business justification for each owner account" }, + { "title": "Downgrade any owners who do not require full organization-level privileges to member or team maintainer roles" }, + { "title": "Ensure remaining owners have MFA enabled and use strong authentication" }, + { "title": "Schedule a periodic review of organization ownership at least annually" } + ] + } + }, + { + "name": "Organization owner data is missing", + "title": "Missing Organization Owner Telemetry Prevents Privileged Access Review", + "statement": "When the organization owner list is missing from collected GitHub data, the policy cannot verify whether ownership is limited to a small, accountable group. Missing owner telemetry can hide excessive privileged access and prevents reviewers from confirming that organization-level administrative authority is appropriately governed.", + "likelihood_hint": "moderate", + "impact_hint": "high", + "violation_ids": ["owners_missing"], + "threat_refs": [ + { + "system": "https://cwe.mitre.org", + "external_id": "CWE-269", + "title": "Improper Privilege Management", + "url": "https://cwe.mitre.org/data/definitions/269.html" + }, + { + "system": "https://cwe.mitre.org", + "external_id": "CWE-284", + "title": "Improper Access Control", + "url": "https://cwe.mitre.org/data/definitions/284.html" + } + ], + "remediation": { + "title": "Collect organization owner data before evaluating owner-count posture", + "description": "Update the GitHub organization data collector or input mapping so the policy receives the current list of organization owners. Re-run the policy after owner telemetry is present to verify that the owner count is compliant.", + "tasks": [ + { "title": "Verify that the GitHub token can read organization owner membership" }, + { "title": "Populate the input owners field with current organization owners" }, + { "title": "Re-run the owner-count policy after collection succeeds" }, + { "title": "Review the collected owner list for stale or unnecessary owner access" } + ] + } + } +] + +_owners := object.get(input, "owners", []) + +violation[{"id": "owners_missing"}] if { + count(_owners) == 0 +} + +violation[{"id": "too_many_owners"}] if { + count(_owners) > 5 +} + +title := "Organization has 5 or fewer owners" +description := "The number of GitHub organization owners should not exceed 5 to limit the blast radius of a privileged account compromise and ensure that elevated access is deliberately granted and regularly reviewed." +remarks := "More information: https://docs.github.com/en/organizations/managing-peoples-access-to-your-organization-with-roles/roles-in-an-organization#organization-owners" diff --git a/policies/gh_org_owner_count_test.rego b/policies/gh_org_owner_count_test.rego new file mode 100644 index 0000000..6342a67 --- /dev/null +++ b/policies/gh_org_owner_count_test.rego @@ -0,0 +1,46 @@ +package compliance_framework.owner_count + +test_owner_count_compliant if { + count(violation) == 0 with input as { + "owners": [ + {"login": "admin1"}, + {"login": "admin2"}, + {"login": "admin3"} + ] + } +} + +test_owner_count_at_limit if { + count(violation) == 0 with input as { + "owners": [ + {"login": "admin1"}, + {"login": "admin2"}, + {"login": "admin3"}, + {"login": "admin4"}, + {"login": "admin5"} + ] + } +} + +test_owner_count_exceeded if { + count(violation) > 0 with input as { + "owners": [ + {"login": "admin1"}, + {"login": "admin2"}, + {"login": "admin3"}, + {"login": "admin4"}, + {"login": "admin5"}, + {"login": "admin6"} + ] + } +} + +test_owner_count_missing_owners if { + count(violation) > 0 with input as {} +} + +test_owner_count_empty_owners if { + count(violation) > 0 with input as { + "owners": [] + } +} diff --git a/policies/gh_org_secret_dependabot_alerts.rego b/policies/gh_org_secret_dependabot_alerts.rego index d9ecea0..abb07f7 100644 --- a/policies/gh_org_secret_dependabot_alerts.rego +++ b/policies/gh_org_secret_dependabot_alerts.rego @@ -4,7 +4,7 @@ risk_templates := [ { "name": "Dependabot alerts not enabled by default for new repositories", "title": "New Repositories Created Without Vulnerability Alert Coverage", - "statement": "When Dependabot alerts are not enabled by default for new repositories, any repository created in the organization will silently accumulate vulnerable dependencies without notification. Security teams have no visibility into known CVEs affecting dependencies in these repositories until alerts are manually enabled, increasing the time-to-detection and the window of exposure.", + "statement": "When no default security configuration has Dependabot alerts enabled for new repositories, any repository created in the organization will silently accumulate vulnerable dependencies without notification. Security teams have no visibility into known CVEs affecting dependencies until alerts are manually enabled, increasing the time-to-detection and the window of exposure.", "likelihood_hint": "moderate", "impact_hint": "high", "violation_ids": ["dependabot_alerts_not_default"], @@ -23,23 +23,58 @@ risk_templates := [ } ], "remediation": { - "title": "Enable Dependabot alerts by default for all new repositories", - "description": "Configure the GitHub organization so that Dependabot alerts are automatically enabled for every new repository created. This ensures that vulnerability detection is active from the moment a repository is initialized.", + "title": "Set a default security configuration with Dependabot alerts enabled for new repositories", + "description": "Create or update a code security configuration in the organization and mark it as the default for new repositories. Ensure Dependabot alerts is set to 'enabled' in that configuration.", "tasks": [ - { "title": "Navigate to Organization Settings > Code security and analysis" }, - { "title": "Enable 'Dependabot alerts' for all new repositories" }, - { "title": "Retroactively enable Dependabot alerts on existing repositories that currently lack coverage" }, - { "title": "Establish a process to review and triage new Dependabot alerts within an agreed SLA" }, - { "title": "Consider also enabling Dependabot security updates to automate remediation PRs" } + { "title": "Navigate to Organization Settings > Security > Advanced Security > Configurations" }, + { "title": "Create or edit a security configuration and enable 'Dependabot alerts'" }, + { "title": "Set the configuration as default for new repositories (public, private, or all)" }, + { "title": "Retroactively apply the configuration to existing repositories that lack coverage" }, + { "title": "Establish a process to review and triage new Dependabot alerts within an agreed SLA" } ] } } ] -violation[{"id": "dependabot_alerts_not_default"}] if { - input.settings.dependabot_alerts_enabled_for_new_repositories == false +_default_security_configs := object.get(input, "default_security_configs", []) + +_dependabot_alerts_enabled_for_all_new_repos if { + some config in _default_security_configs + config.default_for_new_repos == "all" + config.configuration.dependabot_alerts == "enabled" +} + +_dependabot_alerts_enabled_for_all_new_repos if { + some public_config in _default_security_configs + public_config.default_for_new_repos == "public" + public_config.configuration.dependabot_alerts == "enabled" + + some private_config in _default_security_configs + private_config.default_for_new_repos == "private_and_internal" + private_config.configuration.dependabot_alerts == "enabled" +} + +_current_config_summary := summary if { + count(_default_security_configs) == 0 + summary := "No default security configuration is set for the organization." +} + +_current_config_summary := summary if { + count(_default_security_configs) > 0 + entries := [sprintf("'%v' (default_for_new_repos: %v, dependabot_alerts: %v)", [c.configuration.name, c.default_for_new_repos, c.configuration.dependabot_alerts]) | some c in _default_security_configs] + summary := sprintf("Default security configurations found: [%v]", [concat(", ", entries)]) +} + +violation[{ + "id": "dependabot_alerts_not_default", + "description": sprintf( + "Dependabot alerts are not enabled for all new repositories. Expected: an 'all' default configuration with dependabot_alerts = 'enabled', or enabled defaults for both public and private/internal repositories. Current state: %v", + [_current_config_summary] + ) +}] if { + not _dependabot_alerts_enabled_for_all_new_repos } title := "Dependabot alerts enabled for new repositories" -description := "All new repositories should be set up to alert for any dependabot alerts that are coming from the repositories" -remarks := "Endpoint is closing down at some point and moving to code security configurations: See https://docs.github.com/rest/code-security/configurations" +description := "Checks that default code security configurations enable Dependabot alerts for all new repositories in the organization. This requires an 'all' default configuration with 'dependabot_alerts' set to 'enabled', or enabled defaults for both public and private/internal repositories. Configurations are evaluated via GET /orgs/{org}/code-security/configurations/defaults. A configuration with 'dependabot_alerts: not_set' or 'dependabot_alerts: disabled' does not satisfy this requirement." +remarks := "Checked via GET /orgs/{org}/code-security/configurations/defaults. See https://docs.github.com/en/rest/code-security/configurations#get-default-code-security-configurations" diff --git a/policies/gh_org_secret_dependabot_alerts_test.rego b/policies/gh_org_secret_dependabot_alerts_test.rego index 97ee64f..79c849f 100644 --- a/policies/gh_org_secret_dependabot_alerts_test.rego +++ b/policies/gh_org_secret_dependabot_alerts_test.rego @@ -1,17 +1,109 @@ package compliance_framework.dependabot_alerts -test_scanning_enabled_new_repos if { +test_pass_when_all_default_config_has_dependabot_alerts_enabled if { count(violation) == 0 with input as { - "settings": { - "dependabot_alerts_enabled_for_new_repositories": true - } + "default_security_configs": [ + { + "default_for_new_repos": "all", + "configuration": { + "name": "Baseline Security Profile", + "dependabot_alerts": "enabled" + } + } + ] } } -test_secret_scanning_enabled_new_repos_violate_if_disabled if { +test_pass_when_public_and_private_internal_configs_have_dependabot_alerts_enabled if { + count(violation) == 0 with input as { + "default_security_configs": [ + { + "default_for_new_repos": "public", + "configuration": { + "name": "Baseline Security Profile", + "dependabot_alerts": "enabled" + } + }, + { + "default_for_new_repos": "private_and_internal", + "configuration": { + "name": "Private Repos Profile", + "dependabot_alerts": "enabled" + } + } + ] + } +} + +test_violate_when_only_public_config_has_dependabot_alerts_enabled if { + count(violation) > 0 with input as { + "default_security_configs": [ + { + "default_for_new_repos": "public", + "configuration": { + "name": "Baseline Security Profile", + "dependabot_alerts": "enabled" + } + } + ] + } +} + +test_violate_when_private_internal_config_has_dependabot_alerts_disabled if { + count(violation) > 0 with input as { + "default_security_configs": [ + { + "default_for_new_repos": "public", + "configuration": { + "name": "Baseline Security Profile", + "dependabot_alerts": "enabled" + } + }, + { + "default_for_new_repos": "private_and_internal", + "configuration": { + "name": "Private Repos Profile", + "dependabot_alerts": "disabled" + } + } + ] + } +} + +test_violate_when_no_default_configs if { count(violation) > 0 with input as { - "settings": { - "dependabot_alerts_enabled_for_new_repositories": false - } + "default_security_configs": [] } -} \ No newline at end of file +} + +test_violate_when_default_configs_missing if { + count(violation) > 0 with input as {} +} + +test_violate_when_all_configs_have_dependabot_alerts_disabled if { + count(violation) > 0 with input as { + "default_security_configs": [ + { + "default_for_new_repos": "public", + "configuration": { + "name": "Baseline Security Profile", + "dependabot_alerts": "disabled" + } + } + ] + } +} + +test_violate_when_all_configs_have_dependabot_alerts_not_set if { + count(violation) > 0 with input as { + "default_security_configs": [ + { + "default_for_new_repos": "public", + "configuration": { + "name": "Baseline Security Profile", + "dependabot_alerts": "not_set" + } + } + ] + } +} diff --git a/policies/gh_org_secret_scanning_enabled.rego b/policies/gh_org_secret_scanning_enabled.rego index 5789172..fb75e6b 100644 --- a/policies/gh_org_secret_scanning_enabled.rego +++ b/policies/gh_org_secret_scanning_enabled.rego @@ -29,24 +29,59 @@ risk_templates := [ } ], "remediation": { - "title": "Enable secret scanning by default for all new repositories", - "description": "Configure the GitHub organization to automatically enable secret scanning on every new repository. Extend coverage retroactively to existing repositories that currently lack it.", + "title": "Set a default security configuration with secret scanning enabled for new repositories", + "description": "Create or update a code security configuration in the organization and mark it as the default for new repositories. Ensure secret scanning is set to 'enabled' in that configuration.", "tasks": [ - { "title": "Navigate to Organization Settings > Code security and analysis" }, - { "title": "Enable 'Secret scanning' for all new repositories" }, - { "title": "Retroactively enable secret scanning on all existing repositories" }, + { "title": "Navigate to Organization Settings > Security > Advanced Security > Configurations" }, + { "title": "Create or edit a security configuration and enable 'Secret scanning'" }, + { "title": "Set the configuration as default for new repositories (public, private, or all)" }, + { "title": "Retroactively apply the configuration to existing repositories that lack coverage" }, { "title": "Review all existing secret scanning alerts and revoke any exposed credentials immediately" }, - { "title": "Configure push protection at the organization level to block secrets before they enter the repository" }, { "title": "Establish a runbook for responding to secret scanning alerts within an agreed SLA" } ] } } ] -violation[{"id": "secret_scanning_not_default"}] if { - input.settings.secret_scanning_enabled_for_new_repositories == false +_default_security_configs := object.get(input, "default_security_configs", []) + +_secret_scanning_enabled_for_all_new_repos if { + some config in _default_security_configs + config.default_for_new_repos == "all" + config.configuration.secret_scanning == "enabled" +} + +_secret_scanning_enabled_for_all_new_repos if { + some public_config in _default_security_configs + public_config.default_for_new_repos == "public" + public_config.configuration.secret_scanning == "enabled" + + some private_config in _default_security_configs + private_config.default_for_new_repos == "private_and_internal" + private_config.configuration.secret_scanning == "enabled" +} + +_secret_scanning_config_summary := summary if { + count(_default_security_configs) == 0 + summary := "No default security configuration is set for the organization." +} + +_secret_scanning_config_summary := summary if { + count(_default_security_configs) > 0 + entries := [sprintf("'%v' (default_for_new_repos: %v, secret_scanning: %v)", [c.configuration.name, c.default_for_new_repos, c.configuration.secret_scanning]) | some c in _default_security_configs] + summary := sprintf("Default security configurations found: [%v]", [concat(", ", entries)]) +} + +violation[{ + "id": "secret_scanning_not_default", + "description": sprintf( + "Secret scanning is not enabled for all new repositories. Expected: an 'all' default configuration with secret_scanning = 'enabled', or enabled defaults for both public and private/internal repositories. Current state: %v", + [_secret_scanning_config_summary] + ) +}] if { + not _secret_scanning_enabled_for_all_new_repos } title := "Secret Scanning is enabled for new repositories in the organization" -description := "All new repositories should be set up for secret scanning as the default." -remarks := "Endpoint is closing down at some point and moving to code security configurations: See https://docs.github.com/rest/code-security/configurations" +description := "Checks that default code security configurations enable secret scanning for all new repositories in the organization. This requires an 'all' default configuration with 'secret_scanning' set to 'enabled', or enabled defaults for both public and private/internal repositories. Configurations are evaluated via GET /orgs/{org}/code-security/configurations/defaults. A configuration with 'secret_scanning: not_set' or 'secret_scanning: disabled' does not satisfy this requirement." +remarks := "Checked via GET /orgs/{org}/code-security/configurations/defaults. See https://docs.github.com/en/rest/code-security/configurations#get-default-code-security-configurations" diff --git a/policies/gh_org_secret_scanning_enabled_test.rego b/policies/gh_org_secret_scanning_enabled_test.rego index 0612647..a537035 100644 --- a/policies/gh_org_secret_scanning_enabled_test.rego +++ b/policies/gh_org_secret_scanning_enabled_test.rego @@ -1,17 +1,127 @@ package compliance_framework.secret_scanning -test_scanning_enabled_new_repos if { +test_pass_when_all_default_config_has_secret_scanning_enabled if { count(violation) == 0 with input as { - "settings": { - "secret_scanning_enabled_for_new_repositories": true - } + "default_security_configs": [ + { + "default_for_new_repos": "all", + "configuration": { + "name": "Baseline Security Profile", + "secret_scanning": "enabled" + } + } + ] } } -test_secret_scanning_enabled_new_repos_violate_if_disabled if { +test_pass_when_public_and_private_internal_configs_have_secret_scanning_enabled if { + count(violation) == 0 with input as { + "default_security_configs": [ + { + "default_for_new_repos": "public", + "configuration": { + "name": "Baseline Security Profile", + "secret_scanning": "enabled" + } + }, + { + "default_for_new_repos": "private_and_internal", + "configuration": { + "name": "Private Repos Profile", + "secret_scanning": "enabled" + } + } + ] + } +} + +test_violate_when_only_public_config_has_secret_scanning_enabled if { count(violation) > 0 with input as { - "settings": { - "secret_scanning_enabled_for_new_repositories": false - } + "default_security_configs": [ + { + "default_for_new_repos": "public", + "configuration": { + "name": "Baseline Security Profile", + "secret_scanning": "enabled" + } + } + ] + } +} + +test_violate_when_private_internal_config_has_secret_scanning_disabled if { + count(violation) > 0 with input as { + "default_security_configs": [ + { + "default_for_new_repos": "public", + "configuration": { + "name": "Baseline Security Profile", + "secret_scanning": "enabled" + } + }, + { + "default_for_new_repos": "private_and_internal", + "configuration": { + "name": "Private Repos Profile", + "secret_scanning": "disabled" + } + } + ] + } +} + +test_violate_when_no_default_configs if { + count(violation) > 0 with input as { + "default_security_configs": [] + } +} + +test_violate_when_default_configs_missing if { + count(violation) > 0 with input as {} +} + +test_violate_when_all_configs_have_secret_scanning_disabled if { + count(violation) > 0 with input as { + "default_security_configs": [ + { + "default_for_new_repos": "public", + "configuration": { + "name": "Baseline Security Profile", + "secret_scanning": "disabled" + } + } + ] + } +} + +test_violate_when_all_configs_have_secret_scanning_not_set if { + count(violation) > 0 with input as { + "default_security_configs": [ + { + "default_for_new_repos": "all", + "configuration": { + "name": "Baseline Security Profile", + "secret_scanning": "not_set" + } + } + ] + } +} + +test_violation_includes_config_details if { + v := violation with input as { + "default_security_configs": [ + { + "default_for_new_repos": "public", + "configuration": { + "name": "Baseline Security Profile", + "secret_scanning": "disabled" + } + } + ] } + count(v) > 0 + v[entry] + contains(entry.description, "Baseline Security Profile") + contains(entry.description, "secret_scanning: disabled") } diff --git a/policies/gh_org_sso_enabled.rego b/policies/gh_org_sso_enabled.rego new file mode 100644 index 0000000..2874ace --- /dev/null +++ b/policies/gh_org_sso_enabled.rego @@ -0,0 +1,63 @@ +package compliance_framework.sso_enabled + +risk_templates := [ + { + "name": "SAML SSO not enforced for the organization", + "title": "Absence of SSO Enforcement Bypasses Centralized Identity Governance", + "statement": "Without SAML Single Sign-On (SSO) enforcement, organization members can authenticate to GitHub using personal credentials that are independent of the organization's identity provider (IdP). This means that off-boarded employees may retain access after their IdP account is disabled, multi-factor authentication enforcement may be inconsistent, and access auditing is fragmented across GitHub and the IdP. Enforcing SAML SSO ensures that every GitHub session is authenticated through the organization's controlled identity provider, enabling centralized access governance, consistent MFA enforcement, and reliable off-boarding.", + "likelihood_hint": "high", + "impact_hint": "high", + "violation_ids": ["sso_not_enabled"], + "threat_refs": [ + { + "system": "https://cwe.mitre.org", + "external_id": "CWE-287", + "title": "Improper Authentication", + "url": "https://cwe.mitre.org/data/definitions/287.html" + }, + { + "system": "https://cwe.mitre.org", + "external_id": "CWE-306", + "title": "Missing Authentication for Critical Function", + "url": "https://cwe.mitre.org/data/definitions/306.html" + }, + { + "system": "https://cwe.mitre.org", + "external_id": "CWE-522", + "title": "Insufficiently Protected Credentials", + "url": "https://cwe.mitre.org/data/definitions/522.html" + } + ], + "remediation": { + "title": "Enable and enforce SAML SSO for the GitHub organization", + "description": "Configure SAML Single Sign-On for the organization and enforce it so that all members must authenticate via the organization's identity provider. This ensures access is tied to the central IdP lifecycle, enabling reliable off-boarding and consistent MFA.", + "tasks": [ + { "title": "Navigate to Organization Settings > Authentication security > SAML single sign-on" }, + { "title": "Configure your SAML IdP (e.g., Okta, Azure AD, Google Workspace) with the GitHub SSO endpoint" }, + { "title": "Enable SAML SSO and test authentication with a small group before enforcing" }, + { "title": "Enable 'Require SAML SSO authentication' to enforce for all members" }, + { "title": "Communicate the SSO requirement and migration timeline to all organization members" }, + { "title": "Verify that IdP de-provisioning triggers GitHub access revocation" } + ] + } + } +] + +_sso := object.get(input, "sso", {}) + +_sso_enabled := object.get(_sso, "enabled", false) + +_sso_enforced := object.get(_sso, "enforced", false) + +_sso_enabled_and_enforced if { + _sso_enabled + _sso_enforced +} + +violation[{"id": "sso_not_enabled"}] if { + not _sso_enabled_and_enforced +} + +title := "SAML SSO is enabled and enforced for the organization" +description := "The GitHub organization must have SAML Single Sign-On enabled and enforced to ensure all member access is authenticated through the organization's centralized identity provider." +remarks := "More information: https://docs.github.com/en/organizations/managing-saml-single-sign-on-for-your-organization/about-identity-and-access-management-with-saml-single-sign-on" diff --git a/policies/gh_org_sso_enabled_test.rego b/policies/gh_org_sso_enabled_test.rego new file mode 100644 index 0000000..d515987 --- /dev/null +++ b/policies/gh_org_sso_enabled_test.rego @@ -0,0 +1,38 @@ +package compliance_framework.sso_enabled + +test_sso_enabled if { + count(violation) == 0 with input as { + "sso": { + "enabled": true, + "enforced": true, + "sso_url": "https://sso.example.com/saml/github", + "idp_issuer": "https://sso.example.com" + } + } +} + +test_sso_disabled if { + count(violation) == 1 with input as { + "sso": { + "enabled": false, + "enforced": false, + "sso_url": "", + "idp_issuer": "" + } + } +} + +test_sso_enabled_but_not_enforced if { + count(violation) == 1 with input as { + "sso": { + "enabled": true, + "enforced": false, + "sso_url": "https://sso.example.com/saml/github", + "idp_issuer": "https://sso.example.com" + } + } +} + +test_sso_missing if { + count(violation) == 1 with input as {} +} diff --git a/policies/gh_org_team_based_access.rego b/policies/gh_org_team_based_access.rego new file mode 100644 index 0000000..809c9be --- /dev/null +++ b/policies/gh_org_team_based_access.rego @@ -0,0 +1,48 @@ +package compliance_framework.team_based_access + +risk_templates := [ + { + "name": "No teams configured in the organization", + "title": "Absence of Teams Prevents Role-Based Access Control and Access Governance", + "statement": "GitHub teams are the primary mechanism for implementing role-based access control at the repository level within an organization. Without any teams, repository access must be granted directly to individual users, making it impossible to enforce consistent access policies, conduct efficient access reviews, or revoke access at scale during offboarding. An organization with no teams has no structured access governance, increasing the risk of privilege creep and unauthorized access.", + "likelihood_hint": "high", + "impact_hint": "high", + "violation_ids": ["no_teams_configured"], + "threat_refs": [ + { + "system": "https://cwe.mitre.org", + "external_id": "CWE-269", + "title": "Improper Privilege Management", + "url": "https://cwe.mitre.org/data/definitions/269.html" + }, + { + "system": "https://cwe.mitre.org", + "external_id": "CWE-284", + "title": "Improper Access Control", + "url": "https://cwe.mitre.org/data/definitions/284.html" + } + ], + "remediation": { + "title": "Create role-based teams and migrate repository access to team-based grants", + "description": "Establish GitHub teams that reflect your organizational roles (e.g., developers, maintainers, security, admins). Assign repository access at the team level rather than to individual users to enable consistent access policies, efficient onboarding/offboarding, and auditable access reviews.", + "tasks": [ + { "title": "Define the access roles required within the organization (e.g., read, write, maintain, admin)" }, + { "title": "Create a GitHub team for each role with a descriptive name and closed visibility" }, + { "title": "Move all direct-user repository access grants to the appropriate team" }, + { "title": "Remove direct user collaborator access from repositories in favour of team-based grants" }, + { "title": "Document team ownership and responsibility in the team description" }, + { "title": "Establish a process for adding/removing users via team membership changes" } + ] + } + } +] + +_teams := object.get(input, "teams", []) + +violation[{"id": "no_teams_configured"}] if { + count(_teams) == 0 +} + +title := "Organization has at least one team configured for role-based access control" +description := "The organization must have at least one team defined to support role-based access control. Repository access should be granted via teams rather than directly to individual users." +remarks := "More information: https://docs.github.com/en/organizations/organizing-members-into-teams/about-teams" diff --git a/policies/gh_org_team_based_access_test.rego b/policies/gh_org_team_based_access_test.rego new file mode 100644 index 0000000..cf8fa59 --- /dev/null +++ b/policies/gh_org_team_based_access_test.rego @@ -0,0 +1,20 @@ +package compliance_framework.team_based_access + +test_teams_present if { + count(violation) == 0 with input as { + "teams": [ + {"name": "developers", "privacy": "closed"}, + {"name": "security", "privacy": "closed"} + ] + } +} + +test_no_teams if { + count(violation) > 0 with input as { + "teams": [] + } +} + +test_teams_missing if { + count(violation) > 0 with input as {} +} diff --git a/policies/gh_org_web_commit_signoff.rego b/policies/gh_org_web_commit_signoff.rego new file mode 100644 index 0000000..60f713a --- /dev/null +++ b/policies/gh_org_web_commit_signoff.rego @@ -0,0 +1,48 @@ +package compliance_framework.web_commit_signoff + +risk_templates := [ + { + "name": "Web commit sign-off not required", + "title": "Unsigned Web Commits Undermine Change Attribution and Audit Trail Integrity", + "statement": "When web commit sign-off is not enforced, commits made via the GitHub web interface lack a Developer Certificate of Origin (DCO) sign-off, which reduces the integrity of the change attribution record. In compliance contexts, every code change should be traceable to an accountable individual. Without sign-off enforcement, commits made through the web UI can bypass the DCO attestation expected for change management controls, creating gaps in the audit trail.", + "likelihood_hint": "moderate", + "impact_hint": "moderate", + "violation_ids": ["web_commit_signoff_not_required"], + "threat_refs": [ + { + "system": "https://cwe.mitre.org", + "external_id": "CWE-778", + "title": "Insufficient Logging", + "url": "https://cwe.mitre.org/data/definitions/778.html" + }, + { + "system": "https://cwe.mitre.org", + "external_id": "CWE-345", + "title": "Insufficient Verification of Data Authenticity", + "url": "https://cwe.mitre.org/data/definitions/345.html" + } + ], + "remediation": { + "title": "Enable web commit sign-off for the organization", + "description": "Configure the GitHub organization to require a sign-off on all commits made via the web interface. This ensures that every commit carries a Developer Certificate of Origin, maintaining a complete and attributable audit trail for change management compliance.", + "tasks": [ + { "title": "Navigate to Organization Settings > Repository defaults" }, + { "title": "Enable 'Require contributors to sign off on web-based commits'" }, + { "title": "Communicate the sign-off requirement to all contributors who use the GitHub web editor" }, + { "title": "Consider also enforcing GPG or SSH commit signing for locally-pushed commits via branch protection rules" } + ] + } + } +] + +_settings := object.get(input, "settings", {}) + +_web_commit_signoff_required := object.get(_settings, "web_commit_signoff_required", false) + +violation[{"id": "web_commit_signoff_not_required"}] if { + not _web_commit_signoff_required +} + +title := "Web commit sign-off is required for the organization" +description := "All commits made via the GitHub web interface must carry a sign-off to ensure attribution integrity and support change management audit trail requirements." +remarks := "More information: https://docs.github.com/en/organizations/managing-organization-settings/managing-the-commit-signoff-policy-for-your-organization" diff --git a/policies/gh_org_web_commit_signoff_test.rego b/policies/gh_org_web_commit_signoff_test.rego new file mode 100644 index 0000000..b61eb09 --- /dev/null +++ b/policies/gh_org_web_commit_signoff_test.rego @@ -0,0 +1,21 @@ +package compliance_framework.web_commit_signoff + +test_web_commit_signoff_required if { + count(violation) == 0 with input as { + "settings": { + "web_commit_signoff_required": true + } + } +} + +test_web_commit_signoff_not_required if { + count(violation) > 0 with input as { + "settings": { + "web_commit_signoff_required": false + } + } +} + +test_web_commit_signoff_missing if { + count(violation) > 0 with input as {} +}