From 998c5afca46aab89831bbac10855aeb8c317d54b Mon Sep 17 00:00:00 2001 From: Gustavo Carvalho Date: Tue, 5 May 2026 09:52:30 -0300 Subject: [PATCH 01/12] feat: add more policies for better compliance posture for SOC2 fix: support new format for dependabot and secret scanning policies Signed-off-by: Gustavo Carvalho --- example-data/testorg-unremediated.json | 17 ++++- example-data/testorg.json | 15 +++- policies/gh_org_admin_count.rego | 45 ++++++++++++ policies/gh_org_admin_count_test.rego | 36 +++++++++ policies/gh_org_default_repo_permission.rego | 53 ++++++++++++++ .../gh_org_default_repo_permission_test.rego | 33 +++++++++ policies/gh_org_ip_allowlist_enabled.rego | 51 +++++++++++++ .../gh_org_ip_allowlist_enabled_test.rego | 25 +++++++ policies/gh_org_members_can_create_repos.rego | 45 ++++++++++++ .../gh_org_members_can_create_repos_test.rego | 17 +++++ policies/gh_org_secret_dependabot_alerts.rego | 46 +++++++++--- .../gh_org_secret_dependabot_alerts_test.rego | 69 ++++++++++++++++-- policies/gh_org_secret_scanning_enabled.rego | 42 ++++++++--- .../gh_org_secret_scanning_enabled_test.rego | 73 +++++++++++++++++-- policies/gh_org_sso_enabled.rego | 52 +++++++++++++ policies/gh_org_sso_enabled_test.rego | 21 ++++++ policies/gh_org_team_based_access.rego | 46 ++++++++++++ policies/gh_org_team_based_access_test.rego | 16 ++++ policies/gh_org_web_commit_signoff.rego | 44 +++++++++++ policies/gh_org_web_commit_signoff_test.rego | 17 +++++ 20 files changed, 723 insertions(+), 40 deletions(-) create mode 100644 policies/gh_org_admin_count.rego create mode 100644 policies/gh_org_admin_count_test.rego create mode 100644 policies/gh_org_default_repo_permission.rego create mode 100644 policies/gh_org_default_repo_permission_test.rego create mode 100644 policies/gh_org_ip_allowlist_enabled.rego create mode 100644 policies/gh_org_ip_allowlist_enabled_test.rego create mode 100644 policies/gh_org_members_can_create_repos.rego create mode 100644 policies/gh_org_members_can_create_repos_test.rego create mode 100644 policies/gh_org_sso_enabled.rego create mode 100644 policies/gh_org_sso_enabled_test.rego create mode 100644 policies/gh_org_team_based_access.rego create mode 100644 policies/gh_org_team_based_access_test.rego create mode 100644 policies/gh_org_web_commit_signoff.rego create mode 100644 policies/gh_org_web_commit_signoff_test.rego diff --git a/example-data/testorg-unremediated.json b/example-data/testorg-unremediated.json index bd7926c..16c7efd 100644 --- a/example-data/testorg-unremediated.json +++ b/example-data/testorg-unremediated.json @@ -59,5 +59,20 @@ "secret_scanning_push_protection_custom_link_enabled": false, "secret_scanning_push_protection_custom_link": null, "secret_scanning_validity_checks_enabled": false - } + }, + "members": [ + {"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} + ], + "sso": { + "enabled": false, + "sso_url": "", + "idp_issuer": "" + }, + "ip_allow_list": [] } diff --git a/example-data/testorg.json b/example-data/testorg.json index 946e674..617b10d 100644 --- a/example-data/testorg.json +++ b/example-data/testorg.json @@ -59,5 +59,18 @@ "secret_scanning_push_protection_custom_link_enabled": true, "secret_scanning_push_protection_custom_link": null, "secret_scanning_validity_checks_enabled": true - } + }, + "members": [ + {"login": "admin-user-1", "id": 1001}, + {"login": "admin-user-2", "id": 1002} + ], + "sso": { + "enabled": true, + "sso_url": "https://sso.example.com/saml/github", + "idp_issuer": "https://sso.example.com" + }, + "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_admin_count.rego b/policies/gh_org_admin_count.rego new file mode 100644 index 0000000..42feb86 --- /dev/null +++ b/policies/gh_org_admin_count.rego @@ -0,0 +1,45 @@ +package compliance_framework.admin_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_admins"], + "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" } + ] + } + } +] + +violation[{"id": "too_many_admins"}] if { + count(input.members) > 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_admin_count_test.rego b/policies/gh_org_admin_count_test.rego new file mode 100644 index 0000000..78eb204 --- /dev/null +++ b/policies/gh_org_admin_count_test.rego @@ -0,0 +1,36 @@ +package compliance_framework.admin_count + +test_admin_count_compliant if { + count(violation) == 0 with input as { + "members": [ + {"login": "admin1"}, + {"login": "admin2"}, + {"login": "admin3"} + ] + } +} + +test_admin_count_at_limit if { + count(violation) == 0 with input as { + "members": [ + {"login": "admin1"}, + {"login": "admin2"}, + {"login": "admin3"}, + {"login": "admin4"}, + {"login": "admin5"} + ] + } +} + +test_admin_count_exceeded if { + count(violation) > 0 with input as { + "members": [ + {"login": "admin1"}, + {"login": "admin2"}, + {"login": "admin3"}, + {"login": "admin4"}, + {"login": "admin5"}, + {"login": "admin6"} + ] + } +} diff --git a/policies/gh_org_default_repo_permission.rego b/policies/gh_org_default_repo_permission.rego new file mode 100644 index 0000000..786298e --- /dev/null +++ b/policies/gh_org_default_repo_permission.rego @@ -0,0 +1,53 @@ +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" } + ] + } + } +] + +_permissive_permissions := {"write", "admin"} + +violation[{"id": "default_permission_too_permissive"}] if { + _permissive_permissions[input.settings.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..69b1731 --- /dev/null +++ b/policies/gh_org_default_repo_permission_test.rego @@ -0,0 +1,33 @@ +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" + } + } +} diff --git a/policies/gh_org_ip_allowlist_enabled.rego b/policies/gh_org_ip_allowlist_enabled.rego new file mode 100644 index 0000000..97ca818 --- /dev/null +++ b/policies/gh_org_ip_allowlist_enabled.rego @@ -0,0 +1,51 @@ +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" } + ] + } + } +] + +_has_active_entry if { + some entry in input.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..8944eb1 --- /dev/null +++ b/policies/gh_org_ip_allowlist_enabled_test.rego @@ -0,0 +1,25 @@ +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": [] + } +} 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..c714242 --- /dev/null +++ b/policies/gh_org_members_can_create_repos.rego @@ -0,0 +1,45 @@ +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": "Set 'Base permissions' for repository creation to 'None' or restrict to admins only" }, + { "title": "Disable 'Allow members to create repositories' under Repository creation" }, + { "title": "Review and archive any repositories created without administrative approval" }, + { "title": "Document a repository provisioning process that routes requests through an administrator" } + ] + } + } +] + +violation[{"id": "members_can_create_repos"}] if { + input.settings.members_can_create_repositories == true +} + +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..1fedf3d --- /dev/null +++ b/policies/gh_org_members_can_create_repos_test.rego @@ -0,0 +1,17 @@ +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 + } + } +} diff --git a/policies/gh_org_secret_dependabot_alerts.rego b/policies/gh_org_secret_dependabot_alerts.rego index d9ecea0..3a677b3 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,45 @@ 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 +_dependabot_alerts_default_enabled if { + some config in input.default_security_configs + config.configuration.dependabot_alerts == "enabled" +} + +_current_config_summary := summary if { + count(input.default_security_configs) == 0 + summary := "No default security configuration is set for the organization." +} + +_current_config_summary := summary if { + count(input.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 input.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 in any default security configuration. Expected: at least one default configuration with dependabot_alerts = 'enabled'. Current state: %v", + [_current_config_summary] + ), +}] if { + not _dependabot_alerts_default_enabled } 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 at least one default code security configuration exists for the organization with 'dependabot_alerts' set to 'enabled'. This ensures new repositories automatically receive vulnerability alert coverage without manual intervention. 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..9538aa4 100644 --- a/policies/gh_org_secret_dependabot_alerts_test.rego +++ b/policies/gh_org_secret_dependabot_alerts_test.rego @@ -1,17 +1,70 @@ package compliance_framework.dependabot_alerts -test_scanning_enabled_new_repos if { +test_pass_when_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": "public", + "configuration": { + "name": "Baseline Security Profile", + "dependabot_alerts": "enabled" + } + } + ] } } -test_secret_scanning_enabled_new_repos_violate_if_disabled if { +test_pass_when_one_of_multiple_configs_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" + } + }, + { + "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 { + "default_security_configs": [] + } +} + +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 { - "settings": { - "dependabot_alerts_enabled_for_new_repositories": false - } + "default_security_configs": [ + { + "default_for_new_repos": "public", + "configuration": { + "name": "Baseline Security Profile", + "dependabot_alerts": "not_set" + } + } + ] } } \ No newline at end of file diff --git a/policies/gh_org_secret_scanning_enabled.rego b/policies/gh_org_secret_scanning_enabled.rego index 5789172..bf560a4 100644 --- a/policies/gh_org_secret_scanning_enabled.rego +++ b/policies/gh_org_secret_scanning_enabled.rego @@ -29,24 +29,46 @@ 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 +_secret_scanning_default_enabled if { + some config in input.default_security_configs + config.configuration.secret_scanning == "enabled" +} + +_secret_scanning_config_summary := summary if { + count(input.default_security_configs) == 0 + summary := "No default security configuration is set for the organization." +} + +_secret_scanning_config_summary := summary if { + count(input.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 input.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 in any default security configuration. Expected: at least one default configuration with secret_scanning = 'enabled'. Current state: %v", + [_secret_scanning_config_summary] + ), +}] if { + not _secret_scanning_default_enabled } 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 at least one default code security configuration exists for the organization with 'secret_scanning' set to 'enabled'. This ensures new repositories automatically receive secret detection coverage without manual intervention. 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..9e50e6e 100644 --- a/policies/gh_org_secret_scanning_enabled_test.rego +++ b/policies/gh_org_secret_scanning_enabled_test.rego @@ -1,17 +1,74 @@ package compliance_framework.secret_scanning -test_scanning_enabled_new_repos if { +test_pass_when_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": "public", + "configuration": { + "name": "Baseline Security Profile", + "secret_scanning": "enabled" + } + } + ] } } -test_secret_scanning_enabled_new_repos_violate_if_disabled if { +test_pass_when_one_of_multiple_configs_has_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": "disabled" + } + } + ] + } +} + +test_violate_when_no_default_configs if { count(violation) > 0 with input as { - "settings": { - "secret_scanning_enabled_for_new_repositories": false - } + "default_security_configs": [] + } +} + +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_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..d464c4c --- /dev/null +++ b/policies/gh_org_sso_enabled.rego @@ -0,0 +1,52 @@ +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" } + ] + } + } +] + +violation[{"id": "sso_not_enabled"}] if { + input.sso.enabled == false +} + +title := "SAML SSO is enabled 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..a0f1721 --- /dev/null +++ b/policies/gh_org_sso_enabled_test.rego @@ -0,0 +1,21 @@ +package compliance_framework.sso_enabled + +test_sso_enabled if { + count(violation) == 0 with input as { + "sso": { + "enabled": true, + "sso_url": "https://sso.example.com/saml/github", + "idp_issuer": "https://sso.example.com" + } + } +} + +test_sso_disabled if { + count(violation) > 0 with input as { + "sso": { + "enabled": false, + "sso_url": "", + "idp_issuer": "" + } + } +} diff --git a/policies/gh_org_team_based_access.rego b/policies/gh_org_team_based_access.rego new file mode 100644 index 0000000..4f5c218 --- /dev/null +++ b/policies/gh_org_team_based_access.rego @@ -0,0 +1,46 @@ +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" } + ] + } + } +] + +violation[{"id": "no_teams_configured"}] if { + count(input.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..f42bff4 --- /dev/null +++ b/policies/gh_org_team_based_access_test.rego @@ -0,0 +1,16 @@ +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": [] + } +} diff --git a/policies/gh_org_web_commit_signoff.rego b/policies/gh_org_web_commit_signoff.rego new file mode 100644 index 0000000..3436479 --- /dev/null +++ b/policies/gh_org_web_commit_signoff.rego @@ -0,0 +1,44 @@ +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 bypass the attribution guarantees that signed commits provide, creating gaps in the audit trail for change management controls.", + "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" } + ] + } + } +] + +violation[{"id": "web_commit_signoff_not_required"}] if { + input.settings.web_commit_signoff_required == false +} + +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..d4449da --- /dev/null +++ b/policies/gh_org_web_commit_signoff_test.rego @@ -0,0 +1,17 @@ +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 + } + } +} From e9553d8dd82b615a968141e2f41b4a51861e562f Mon Sep 17 00:00:00 2001 From: Gustavo Carvalho Date: Tue, 5 May 2026 11:10:56 -0300 Subject: [PATCH 02/12] fix: copilot issues Signed-off-by: Gustavo Carvalho --- example-data/testorg-unremediated.json | 14 +++++++++++++- example-data/testorg.json | 17 ++++++++++++++++- policies/gh_org_admin_count.rego | 8 +++++--- policies/gh_org_admin_count_test.rego | 6 +++--- policies/gh_org_members_can_create_repos.rego | 4 ++-- policies/gh_org_secret_dependabot_alerts.rego | 10 ++++++---- .../gh_org_secret_dependabot_alerts_test.rego | 6 +++++- policies/gh_org_secret_scanning_enabled.rego | 10 ++++++---- .../gh_org_secret_scanning_enabled_test.rego | 4 ++++ policies/gh_org_sso_enabled.rego | 12 +++++++++++- policies/gh_org_sso_enabled_test.rego | 13 +++++++++++++ policies/gh_org_team_based_access.rego | 4 +++- policies/gh_org_team_based_access_test.rego | 4 ++++ 13 files changed, 91 insertions(+), 21 deletions(-) diff --git a/example-data/testorg-unremediated.json b/example-data/testorg-unremediated.json index 16c7efd..7cd5cbf 100644 --- a/example-data/testorg-unremediated.json +++ b/example-data/testorg-unremediated.json @@ -60,7 +60,7 @@ "secret_scanning_push_protection_custom_link": null, "secret_scanning_validity_checks_enabled": false }, - "members": [ + "owners": [ {"login": "admin-user-1", "id": 1001}, {"login": "admin-user-2", "id": 1002}, {"login": "admin-user-3", "id": 1003}, @@ -69,10 +69,22 @@ {"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 617b10d..4c961fd 100644 --- a/example-data/testorg.json +++ b/example-data/testorg.json @@ -60,15 +60,30 @@ "secret_scanning_push_protection_custom_link": null, "secret_scanning_validity_checks_enabled": true }, - "members": [ + "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_admin_count.rego b/policies/gh_org_admin_count.rego index 42feb86..1e00633 100644 --- a/policies/gh_org_admin_count.rego +++ b/policies/gh_org_admin_count.rego @@ -7,7 +7,7 @@ risk_templates := [ "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_admins"], + "violation_ids": ["too_many_owners"], "threat_refs": [ { "system": "https://cwe.mitre.org", @@ -36,8 +36,10 @@ risk_templates := [ } ] -violation[{"id": "too_many_admins"}] if { - count(input.members) > 5 +_owners := object.get(input, "owners", []) + +violation[{"id": "too_many_owners"}] if { + count(_owners) > 5 } title := "Organization has 5 or fewer owners" diff --git a/policies/gh_org_admin_count_test.rego b/policies/gh_org_admin_count_test.rego index 78eb204..65c94ae 100644 --- a/policies/gh_org_admin_count_test.rego +++ b/policies/gh_org_admin_count_test.rego @@ -2,7 +2,7 @@ package compliance_framework.admin_count test_admin_count_compliant if { count(violation) == 0 with input as { - "members": [ + "owners": [ {"login": "admin1"}, {"login": "admin2"}, {"login": "admin3"} @@ -12,7 +12,7 @@ test_admin_count_compliant if { test_admin_count_at_limit if { count(violation) == 0 with input as { - "members": [ + "owners": [ {"login": "admin1"}, {"login": "admin2"}, {"login": "admin3"}, @@ -24,7 +24,7 @@ test_admin_count_at_limit if { test_admin_count_exceeded if { count(violation) > 0 with input as { - "members": [ + "owners": [ {"login": "admin1"}, {"login": "admin2"}, {"login": "admin3"}, diff --git a/policies/gh_org_members_can_create_repos.rego b/policies/gh_org_members_can_create_repos.rego index c714242..b073487 100644 --- a/policies/gh_org_members_can_create_repos.rego +++ b/policies/gh_org_members_can_create_repos.rego @@ -27,8 +27,8 @@ risk_templates := [ "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": "Set 'Base permissions' for repository creation to 'None' or restrict to admins only" }, - { "title": "Disable 'Allow members to create repositories' under Repository creation" }, + { "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" } ] diff --git a/policies/gh_org_secret_dependabot_alerts.rego b/policies/gh_org_secret_dependabot_alerts.rego index 3a677b3..77d2e02 100644 --- a/policies/gh_org_secret_dependabot_alerts.rego +++ b/policies/gh_org_secret_dependabot_alerts.rego @@ -36,19 +36,21 @@ risk_templates := [ } ] +_default_security_configs := object.get(input, "default_security_configs", []) + _dependabot_alerts_default_enabled if { - some config in input.default_security_configs + some config in _default_security_configs config.configuration.dependabot_alerts == "enabled" } _current_config_summary := summary if { - count(input.default_security_configs) == 0 + count(_default_security_configs) == 0 summary := "No default security configuration is set for the organization." } _current_config_summary := summary if { - count(input.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 input.default_security_configs] + 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)]) } diff --git a/policies/gh_org_secret_dependabot_alerts_test.rego b/policies/gh_org_secret_dependabot_alerts_test.rego index 9538aa4..fbb59e1 100644 --- a/policies/gh_org_secret_dependabot_alerts_test.rego +++ b/policies/gh_org_secret_dependabot_alerts_test.rego @@ -41,6 +41,10 @@ test_violate_when_no_default_configs if { } } +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": [ @@ -67,4 +71,4 @@ test_violate_when_all_configs_have_dependabot_alerts_not_set if { } ] } -} \ No newline at end of file +} diff --git a/policies/gh_org_secret_scanning_enabled.rego b/policies/gh_org_secret_scanning_enabled.rego index bf560a4..d3dd029 100644 --- a/policies/gh_org_secret_scanning_enabled.rego +++ b/policies/gh_org_secret_scanning_enabled.rego @@ -43,19 +43,21 @@ risk_templates := [ } ] +_default_security_configs := object.get(input, "default_security_configs", []) + _secret_scanning_default_enabled if { - some config in input.default_security_configs + some config in _default_security_configs config.configuration.secret_scanning == "enabled" } _secret_scanning_config_summary := summary if { - count(input.default_security_configs) == 0 + count(_default_security_configs) == 0 summary := "No default security configuration is set for the organization." } _secret_scanning_config_summary := summary if { - count(input.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 input.default_security_configs] + 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)]) } diff --git a/policies/gh_org_secret_scanning_enabled_test.rego b/policies/gh_org_secret_scanning_enabled_test.rego index 9e50e6e..a9780f5 100644 --- a/policies/gh_org_secret_scanning_enabled_test.rego +++ b/policies/gh_org_secret_scanning_enabled_test.rego @@ -41,6 +41,10 @@ test_violate_when_no_default_configs if { } } +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": [ diff --git a/policies/gh_org_sso_enabled.rego b/policies/gh_org_sso_enabled.rego index d464c4c..d842bfd 100644 --- a/policies/gh_org_sso_enabled.rego +++ b/policies/gh_org_sso_enabled.rego @@ -43,8 +43,18 @@ risk_templates := [ } ] +_sso := object.get(input, "sso", {}) + +_sso_enabled := object.get(_sso, "enabled", false) + +_sso_enforced := object.get(_sso, "enforced", false) + +violation[{"id": "sso_not_enabled"}] if { + not _sso_enabled +} + violation[{"id": "sso_not_enabled"}] if { - input.sso.enabled == false + not _sso_enforced } title := "SAML SSO is enabled for the organization" diff --git a/policies/gh_org_sso_enabled_test.rego b/policies/gh_org_sso_enabled_test.rego index a0f1721..6ca805b 100644 --- a/policies/gh_org_sso_enabled_test.rego +++ b/policies/gh_org_sso_enabled_test.rego @@ -4,6 +4,7 @@ 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" } @@ -14,8 +15,20 @@ test_sso_disabled if { count(violation) > 0 with input as { "sso": { "enabled": false, + "enforced": false, "sso_url": "", "idp_issuer": "" } } } + +test_sso_enabled_but_not_enforced if { + count(violation) > 0 with input as { + "sso": { + "enabled": true, + "enforced": false, + "sso_url": "https://sso.example.com/saml/github", + "idp_issuer": "https://sso.example.com" + } + } +} diff --git a/policies/gh_org_team_based_access.rego b/policies/gh_org_team_based_access.rego index 4f5c218..809c9be 100644 --- a/policies/gh_org_team_based_access.rego +++ b/policies/gh_org_team_based_access.rego @@ -37,8 +37,10 @@ risk_templates := [ } ] +_teams := object.get(input, "teams", []) + violation[{"id": "no_teams_configured"}] if { - count(input.teams) == 0 + count(_teams) == 0 } title := "Organization has at least one team configured for role-based access control" diff --git a/policies/gh_org_team_based_access_test.rego b/policies/gh_org_team_based_access_test.rego index f42bff4..cf8fa59 100644 --- a/policies/gh_org_team_based_access_test.rego +++ b/policies/gh_org_team_based_access_test.rego @@ -14,3 +14,7 @@ test_no_teams if { "teams": [] } } + +test_teams_missing if { + count(violation) > 0 with input as {} +} From 02a0a270afbd4345ae6e722bcde707a870851520 Mon Sep 17 00:00:00 2001 From: Gustavo Carvalho Date: Tue, 5 May 2026 17:37:06 -0300 Subject: [PATCH 03/12] fix: copilot issues Signed-off-by: Gustavo Carvalho --- policies/gh_org_ip_allowlist_enabled.rego | 4 +++- policies/gh_org_ip_allowlist_enabled_test.rego | 4 ++++ policies/gh_org_sso_enabled.rego | 7 ++++--- policies/gh_org_sso_enabled_test.rego | 8 ++++++-- 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/policies/gh_org_ip_allowlist_enabled.rego b/policies/gh_org_ip_allowlist_enabled.rego index 97ca818..214c79e 100644 --- a/policies/gh_org_ip_allowlist_enabled.rego +++ b/policies/gh_org_ip_allowlist_enabled.rego @@ -37,8 +37,10 @@ risk_templates := [ } ] +_ip_allow_list := object.get(input, "ip_allow_list", []) + _has_active_entry if { - some entry in input.ip_allow_list + some entry in _ip_allow_list entry.is_active == true } diff --git a/policies/gh_org_ip_allowlist_enabled_test.rego b/policies/gh_org_ip_allowlist_enabled_test.rego index 8944eb1..b5ce7a4 100644 --- a/policies/gh_org_ip_allowlist_enabled_test.rego +++ b/policies/gh_org_ip_allowlist_enabled_test.rego @@ -23,3 +23,7 @@ test_ip_allowlist_empty if { "ip_allow_list": [] } } + +test_ip_allowlist_missing if { + count(violation) > 0 with input as {} +} diff --git a/policies/gh_org_sso_enabled.rego b/policies/gh_org_sso_enabled.rego index d842bfd..39b7ef2 100644 --- a/policies/gh_org_sso_enabled.rego +++ b/policies/gh_org_sso_enabled.rego @@ -49,12 +49,13 @@ _sso_enabled := object.get(_sso, "enabled", false) _sso_enforced := object.get(_sso, "enforced", false) -violation[{"id": "sso_not_enabled"}] if { - not _sso_enabled +_sso_enabled_and_enforced if { + _sso_enabled + _sso_enforced } violation[{"id": "sso_not_enabled"}] if { - not _sso_enforced + not _sso_enabled_and_enforced } title := "SAML SSO is enabled for the organization" diff --git a/policies/gh_org_sso_enabled_test.rego b/policies/gh_org_sso_enabled_test.rego index 6ca805b..d515987 100644 --- a/policies/gh_org_sso_enabled_test.rego +++ b/policies/gh_org_sso_enabled_test.rego @@ -12,7 +12,7 @@ test_sso_enabled if { } test_sso_disabled if { - count(violation) > 0 with input as { + count(violation) == 1 with input as { "sso": { "enabled": false, "enforced": false, @@ -23,7 +23,7 @@ test_sso_disabled if { } test_sso_enabled_but_not_enforced if { - count(violation) > 0 with input as { + count(violation) == 1 with input as { "sso": { "enabled": true, "enforced": false, @@ -32,3 +32,7 @@ test_sso_enabled_but_not_enforced if { } } } + +test_sso_missing if { + count(violation) == 1 with input as {} +} From 77ff1719345dcdc0ae1e81b309aa25102dcf1aa4 Mon Sep 17 00:00:00 2001 From: Gustavo Carvalho Date: Tue, 5 May 2026 17:50:22 -0300 Subject: [PATCH 04/12] fix: copilot issues Signed-off-by: Gustavo Carvalho --- example-data/testorg.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/example-data/testorg.json b/example-data/testorg.json index 4c961fd..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, From 516afcf78513b48811df7211ab5bc1621193cdf8 Mon Sep 17 00:00:00 2001 From: Gustavo Carvalho Date: Tue, 5 May 2026 17:58:56 -0300 Subject: [PATCH 05/12] fix: copilot issues Signed-off-by: Gustavo Carvalho --- policies/gh_org_sso_enabled.rego | 2 +- policies/gh_org_web_commit_signoff.rego | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/policies/gh_org_sso_enabled.rego b/policies/gh_org_sso_enabled.rego index 39b7ef2..2874ace 100644 --- a/policies/gh_org_sso_enabled.rego +++ b/policies/gh_org_sso_enabled.rego @@ -58,6 +58,6 @@ violation[{"id": "sso_not_enabled"}] if { not _sso_enabled_and_enforced } -title := "SAML SSO is enabled for the organization" +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_web_commit_signoff.rego b/policies/gh_org_web_commit_signoff.rego index 3436479..1a4db0f 100644 --- a/policies/gh_org_web_commit_signoff.rego +++ b/policies/gh_org_web_commit_signoff.rego @@ -4,7 +4,7 @@ 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 bypass the attribution guarantees that signed commits provide, creating gaps in the audit trail for change management controls.", + "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"], From d0f05582bd68eca05c07870d7ac2541ac3951436 Mon Sep 17 00:00:00 2001 From: Gustavo Carvalho Date: Tue, 5 May 2026 18:25:01 -0300 Subject: [PATCH 06/12] fix: copilot issues Signed-off-by: Gustavo Carvalho --- policies/gh_org_default_repo_permission.rego | 8 ++++++-- policies/gh_org_default_repo_permission_test.rego | 4 ++++ policies/gh_org_members_can_create_repos.rego | 6 +++++- policies/gh_org_members_can_create_repos_test.rego | 4 ++++ policies/gh_org_web_commit_signoff.rego | 6 +++++- policies/gh_org_web_commit_signoff_test.rego | 4 ++++ 6 files changed, 28 insertions(+), 4 deletions(-) diff --git a/policies/gh_org_default_repo_permission.rego b/policies/gh_org_default_repo_permission.rego index 786298e..fe11b24 100644 --- a/policies/gh_org_default_repo_permission.rego +++ b/policies/gh_org_default_repo_permission.rego @@ -42,10 +42,14 @@ risk_templates := [ } ] -_permissive_permissions := {"write", "admin"} +_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 { - _permissive_permissions[input.settings.default_repository_permission] + not _allowed_permissions[_default_repository_permission] } title := "Default repository permission is set to 'read' or 'none'" diff --git a/policies/gh_org_default_repo_permission_test.rego b/policies/gh_org_default_repo_permission_test.rego index 69b1731..74712b4 100644 --- a/policies/gh_org_default_repo_permission_test.rego +++ b/policies/gh_org_default_repo_permission_test.rego @@ -31,3 +31,7 @@ test_default_permission_admin if { } } } + +test_default_permission_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 index b073487..90d4a6f 100644 --- a/policies/gh_org_members_can_create_repos.rego +++ b/policies/gh_org_members_can_create_repos.rego @@ -36,8 +36,12 @@ risk_templates := [ } ] +_settings := object.get(input, "settings", {}) + +_members_can_create_repositories := object.get(_settings, "members_can_create_repositories", true) + violation[{"id": "members_can_create_repos"}] if { - input.settings.members_can_create_repositories == true + _members_can_create_repositories } title := "Organization members cannot create repositories" diff --git a/policies/gh_org_members_can_create_repos_test.rego b/policies/gh_org_members_can_create_repos_test.rego index 1fedf3d..34bf2c9 100644 --- a/policies/gh_org_members_can_create_repos_test.rego +++ b/policies/gh_org_members_can_create_repos_test.rego @@ -15,3 +15,7 @@ test_members_can_create_repos if { } } } + +test_members_create_repos_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 index 1a4db0f..60f713a 100644 --- a/policies/gh_org_web_commit_signoff.rego +++ b/policies/gh_org_web_commit_signoff.rego @@ -35,8 +35,12 @@ risk_templates := [ } ] +_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 { - input.settings.web_commit_signoff_required == false + not _web_commit_signoff_required } title := "Web commit sign-off is required for the organization" diff --git a/policies/gh_org_web_commit_signoff_test.rego b/policies/gh_org_web_commit_signoff_test.rego index d4449da..b61eb09 100644 --- a/policies/gh_org_web_commit_signoff_test.rego +++ b/policies/gh_org_web_commit_signoff_test.rego @@ -15,3 +15,7 @@ test_web_commit_signoff_not_required if { } } } + +test_web_commit_signoff_missing if { + count(violation) > 0 with input as {} +} From 24e7f8253bc0c19dfe40c8aa17f8530ad264cb79 Mon Sep 17 00:00:00 2001 From: Gustavo Carvalho Date: Tue, 5 May 2026 18:34:45 -0300 Subject: [PATCH 07/12] fix: copilot issues Signed-off-by: Gustavo Carvalho --- policies/gh_org_admin_count.rego | 6 +++++- policies/gh_org_admin_count_test.rego | 4 ++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/policies/gh_org_admin_count.rego b/policies/gh_org_admin_count.rego index 1e00633..d535809 100644 --- a/policies/gh_org_admin_count.rego +++ b/policies/gh_org_admin_count.rego @@ -7,7 +7,7 @@ risk_templates := [ "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"], + "violation_ids": ["too_many_owners", "owners_missing"], "threat_refs": [ { "system": "https://cwe.mitre.org", @@ -38,6 +38,10 @@ risk_templates := [ _owners := object.get(input, "owners", []) +violation[{"id": "owners_missing"}] if { + not "owners" in object.keys(input) +} + violation[{"id": "too_many_owners"}] if { count(_owners) > 5 } diff --git a/policies/gh_org_admin_count_test.rego b/policies/gh_org_admin_count_test.rego index 65c94ae..1c5939d 100644 --- a/policies/gh_org_admin_count_test.rego +++ b/policies/gh_org_admin_count_test.rego @@ -34,3 +34,7 @@ test_admin_count_exceeded if { ] } } + +test_admin_count_missing_owners if { + count(violation) > 0 with input as {} +} From e67251049c816d589981a31ba60d715857a2ccb1 Mon Sep 17 00:00:00 2001 From: Gustavo Carvalho Date: Wed, 6 May 2026 05:36:26 -0300 Subject: [PATCH 08/12] fix: copilot issues Signed-off-by: Gustavo Carvalho --- policies/gh_org_secret_dependabot_alerts.rego | 21 +++++++--- .../gh_org_secret_dependabot_alerts_test.rego | 41 +++++++++++++++++-- policies/gh_org_secret_scanning_enabled.rego | 21 +++++++--- .../gh_org_secret_scanning_enabled_test.rego | 41 +++++++++++++++++-- 4 files changed, 108 insertions(+), 16 deletions(-) diff --git a/policies/gh_org_secret_dependabot_alerts.rego b/policies/gh_org_secret_dependabot_alerts.rego index 77d2e02..abb07f7 100644 --- a/policies/gh_org_secret_dependabot_alerts.rego +++ b/policies/gh_org_secret_dependabot_alerts.rego @@ -38,11 +38,22 @@ risk_templates := [ _default_security_configs := object.get(input, "default_security_configs", []) -_dependabot_alerts_default_enabled if { +_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." @@ -57,13 +68,13 @@ _current_config_summary := summary if { violation[{ "id": "dependabot_alerts_not_default", "description": sprintf( - "Dependabot alerts are not enabled in any default security configuration. Expected: at least one default configuration with dependabot_alerts = 'enabled'. Current state: %v", + "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_default_enabled + not _dependabot_alerts_enabled_for_all_new_repos } title := "Dependabot alerts enabled for new repositories" -description := "Checks that at least one default code security configuration exists for the organization with 'dependabot_alerts' set to 'enabled'. This ensures new repositories automatically receive vulnerability alert coverage without manual intervention. 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." +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 fbb59e1..79c849f 100644 --- a/policies/gh_org_secret_dependabot_alerts_test.rego +++ b/policies/gh_org_secret_dependabot_alerts_test.rego @@ -1,10 +1,10 @@ package compliance_framework.dependabot_alerts -test_pass_when_default_config_has_dependabot_alerts_enabled if { +test_pass_when_all_default_config_has_dependabot_alerts_enabled if { count(violation) == 0 with input as { "default_security_configs": [ { - "default_for_new_repos": "public", + "default_for_new_repos": "all", "configuration": { "name": "Baseline Security Profile", "dependabot_alerts": "enabled" @@ -14,8 +14,43 @@ test_pass_when_default_config_has_dependabot_alerts_enabled if { } } -test_pass_when_one_of_multiple_configs_has_dependabot_alerts_enabled 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", diff --git a/policies/gh_org_secret_scanning_enabled.rego b/policies/gh_org_secret_scanning_enabled.rego index d3dd029..fb75e6b 100644 --- a/policies/gh_org_secret_scanning_enabled.rego +++ b/policies/gh_org_secret_scanning_enabled.rego @@ -45,11 +45,22 @@ risk_templates := [ _default_security_configs := object.get(input, "default_security_configs", []) -_secret_scanning_default_enabled if { +_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." @@ -64,13 +75,13 @@ _secret_scanning_config_summary := summary if { violation[{ "id": "secret_scanning_not_default", "description": sprintf( - "Secret scanning is not enabled in any default security configuration. Expected: at least one default configuration with secret_scanning = 'enabled'. Current state: %v", + "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_default_enabled + not _secret_scanning_enabled_for_all_new_repos } title := "Secret Scanning is enabled for new repositories in the organization" -description := "Checks that at least one default code security configuration exists for the organization with 'secret_scanning' set to 'enabled'. This ensures new repositories automatically receive secret detection coverage without manual intervention. 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." +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 a9780f5..6377e5e 100644 --- a/policies/gh_org_secret_scanning_enabled_test.rego +++ b/policies/gh_org_secret_scanning_enabled_test.rego @@ -1,10 +1,10 @@ package compliance_framework.secret_scanning -test_pass_when_default_config_has_secret_scanning_enabled if { +test_pass_when_all_default_config_has_secret_scanning_enabled if { count(violation) == 0 with input as { "default_security_configs": [ { - "default_for_new_repos": "public", + "default_for_new_repos": "all", "configuration": { "name": "Baseline Security Profile", "secret_scanning": "enabled" @@ -14,8 +14,43 @@ test_pass_when_default_config_has_secret_scanning_enabled if { } } -test_pass_when_one_of_multiple_configs_has_secret_scanning_enabled 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 { + "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", From 09e2bd8d87de5fe46014ff6eb3e375e1d6da0984 Mon Sep 17 00:00:00 2001 From: Gustavo Carvalho Date: Wed, 6 May 2026 05:49:06 -0300 Subject: [PATCH 09/12] fix: copilot issues Signed-off-by: Gustavo Carvalho --- policies/gh_org_admin_count.rego | 51 --------------------------- policies/gh_org_admin_count_test.rego | 40 --------------------- 2 files changed, 91 deletions(-) delete mode 100644 policies/gh_org_admin_count.rego delete mode 100644 policies/gh_org_admin_count_test.rego diff --git a/policies/gh_org_admin_count.rego b/policies/gh_org_admin_count.rego deleted file mode 100644 index d535809..0000000 --- a/policies/gh_org_admin_count.rego +++ /dev/null @@ -1,51 +0,0 @@ -package compliance_framework.admin_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", "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": "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" } - ] - } - } -] - -_owners := object.get(input, "owners", []) - -violation[{"id": "owners_missing"}] if { - not "owners" in object.keys(input) -} - -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_admin_count_test.rego b/policies/gh_org_admin_count_test.rego deleted file mode 100644 index 1c5939d..0000000 --- a/policies/gh_org_admin_count_test.rego +++ /dev/null @@ -1,40 +0,0 @@ -package compliance_framework.admin_count - -test_admin_count_compliant if { - count(violation) == 0 with input as { - "owners": [ - {"login": "admin1"}, - {"login": "admin2"}, - {"login": "admin3"} - ] - } -} - -test_admin_count_at_limit if { - count(violation) == 0 with input as { - "owners": [ - {"login": "admin1"}, - {"login": "admin2"}, - {"login": "admin3"}, - {"login": "admin4"}, - {"login": "admin5"} - ] - } -} - -test_admin_count_exceeded if { - count(violation) > 0 with input as { - "owners": [ - {"login": "admin1"}, - {"login": "admin2"}, - {"login": "admin3"}, - {"login": "admin4"}, - {"login": "admin5"}, - {"login": "admin6"} - ] - } -} - -test_admin_count_missing_owners if { - count(violation) > 0 with input as {} -} From 709fbb1aef1bad7d624c81dcf79ad874f1b89b23 Mon Sep 17 00:00:00 2001 From: Gustavo Carvalho Date: Wed, 6 May 2026 05:56:03 -0300 Subject: [PATCH 10/12] fix: forgot to add files after rename Signed-off-by: Gustavo Carvalho --- policies/gh_org_owner_count.rego | 83 +++++++++++++++++++++++++++ policies/gh_org_owner_count_test.rego | 40 +++++++++++++ 2 files changed, 123 insertions(+) create mode 100644 policies/gh_org_owner_count.rego create mode 100644 policies/gh_org_owner_count_test.rego diff --git a/policies/gh_org_owner_count.rego b/policies/gh_org_owner_count.rego new file mode 100644 index 0000000..e7ac120 --- /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 { + not "owners" in object.keys(input) +} + +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..1400b84 --- /dev/null +++ b/policies/gh_org_owner_count_test.rego @@ -0,0 +1,40 @@ +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 {} +} From 50cff282c034dbb17446aa2b081cf6684b7b905b Mon Sep 17 00:00:00 2001 From: Gustavo Carvalho Date: Wed, 6 May 2026 06:18:51 -0300 Subject: [PATCH 11/12] fix: copilot issues Signed-off-by: Gustavo Carvalho --- policies/gh_org_owner_count.rego | 4 ++++ policies/gh_org_owner_count_test.rego | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/policies/gh_org_owner_count.rego b/policies/gh_org_owner_count.rego index e7ac120..da0d900 100644 --- a/policies/gh_org_owner_count.rego +++ b/policies/gh_org_owner_count.rego @@ -74,6 +74,10 @@ violation[{"id": "owners_missing"}] if { not "owners" in object.keys(input) } +violation[{"id": "owners_missing"}] if { + count(_owners) == 0 +} + violation[{"id": "too_many_owners"}] if { count(_owners) > 5 } diff --git a/policies/gh_org_owner_count_test.rego b/policies/gh_org_owner_count_test.rego index 1400b84..6342a67 100644 --- a/policies/gh_org_owner_count_test.rego +++ b/policies/gh_org_owner_count_test.rego @@ -38,3 +38,9 @@ test_owner_count_exceeded if { 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": [] + } +} From 9906b7db7f83a9e0580a7c45df500e0b3585a2d6 Mon Sep 17 00:00:00 2001 From: Gustavo Carvalho Date: Wed, 6 May 2026 06:27:37 -0300 Subject: [PATCH 12/12] fix: copilot issues Signed-off-by: Gustavo Carvalho --- policies/gh_org_owner_count.rego | 4 ---- policies/gh_org_secret_scanning_enabled_test.rego | 14 ++++++++++++++ 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/policies/gh_org_owner_count.rego b/policies/gh_org_owner_count.rego index da0d900..b645f59 100644 --- a/policies/gh_org_owner_count.rego +++ b/policies/gh_org_owner_count.rego @@ -70,10 +70,6 @@ risk_templates := [ _owners := object.get(input, "owners", []) -violation[{"id": "owners_missing"}] if { - not "owners" in object.keys(input) -} - violation[{"id": "owners_missing"}] if { count(_owners) == 0 } diff --git a/policies/gh_org_secret_scanning_enabled_test.rego b/policies/gh_org_secret_scanning_enabled_test.rego index 6377e5e..a537035 100644 --- a/policies/gh_org_secret_scanning_enabled_test.rego +++ b/policies/gh_org_secret_scanning_enabled_test.rego @@ -94,6 +94,20 @@ test_violate_when_all_configs_have_secret_scanning_disabled if { } } +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": [