{
  "generatedAt": "2026-10-12T16:01:05.493Z",
  "effortLabels": {
    "quick-win": "Quick win · under an hour",
    "quarter": "Moderate · plan for this quarter",
    "initiative": "Larger initiative · weeks"
  },
  "cicdSec": {
    "source": "https://owasp.org/www-project-top-10-ci-cd-security-risks/",
    "risks": {
      "1": "Insufficient Flow Control Mechanisms",
      "2": "Inadequate Identity and Access Management",
      "3": "Dependency Chain Abuse",
      "4": "Poisoned Pipeline Execution (PPE)",
      "5": "Insufficient PBAC (Pipeline-Based Access Controls)",
      "6": "Insufficient Credential Hygiene",
      "7": "Insecure System Configuration",
      "8": "Ungoverned Usage of 3rd Party Services",
      "9": "Improper Artifact Integrity Validation",
      "10": "Insufficient Logging and Visibility"
    }
  },
  "planLabels": {
    "free": "Free",
    "team": "Team",
    "enterprise": "Enterprise",
    "enterprise-server": "Enterprise Server"
  },
  "detectSource": "https://docs.github.com/en/enterprise-cloud@latest/admin/monitoring-activity-in-your-enterprise/reviewing-audit-logs-for-your-enterprise/audit-log-events-for-your-enterprise",
  "categories": [
    {
      "category": "Authentication & identity",
      "items": [
        {
          "id": "auth-mfa-org",
          "text": "Enforce MFA for every member (prefer hardware/FIDO2 keys over SMS or app codes)",
          "tier": "quick-win",
          "why": "MFA is the single biggest blocker to account takeover from a leaked or guessed password.",
          "check": {
            "scope": "org",
            "calls": [
              "/orgs/{org}"
            ],
            "jq": ".[0].two_factor_requirement_enabled",
            "detail": "if .[0].two_factor_requirement_enabled == null then \"2FA setting is only visible to organization owners\" else \"two_factor_requirement_enabled: \\(.[0].two_factor_requirement_enabled)\" end",
            "needs": "Organization owner (classic token: admin:org, read:org)",
            "note": "The API does not say whether SMS is blocked (secure methods only), so check that part by hand."
          },
          "prerequisites": [
            "Organization owner"
          ],
          "steps": [
            "Go to `github.com/orgs/ORG/people` and filter by 2FA to see who has it off. Ask them to enable it first, since members without 2FA are removed when you turn this on.",
            "Go to `github.com/organizations/ORG/settings/security`.",
            "Under \"Two-factor authentication\", select \"Require two-factor authentication for everyone in the organization\".",
            "Also select the option to require secure methods only, which blocks SMS. Save.",
            "Ask members to register a security key or passkey as their main method."
          ],
          "docs": {
            "title": "Requiring 2FA in your organization",
            "url": "https://docs.github.com/en/organizations/keeping-your-organization-secure/managing-two-factor-authentication-for-your-organization/requiring-two-factor-authentication-in-your-organization"
          },
          "scenarioIds": [
            "user-circleci-session-theft"
          ],
          "meta": {
            "cicdSec": [
              2
            ],
            "plan": "free"
          },
          "detect": {
            "events": [
              "org.disable_two_factor_requirement",
              "business.disable_two_factor_requirement",
              "business.remove_disallowed_two_factor_method",
              "business.clear_disallowed_two_factor_methods"
            ],
            "match": "exact"
          }
        },
        {
          "id": "auth-disable-password-git",
          "text": "Disable password-based Git authentication (Enterprise Server only)",
          "tier": "quick-win",
          "why": "Password-based Git auth can't be scoped, expired, or tied to one machine the way a key or token can.",
          "prerequisites": [
            "Site administrator"
          ],
          "steps": [
            "On GitHub Enterprise Server, check the Authentication section of the Management Console and turn off password authentication for Git operations if it is still on.",
            "On github.com there is nothing to do: password authentication for Git was removed in 2021, so Git over HTTPS already needs a token. Mark this item done.",
            "Either way, check that people and scripts use SSH keys or fine-grained PATs, not classic PATs pasted in place of a password."
          ],
          "docs": {
            "title": "About authentication to GitHub",
            "url": "https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/about-authentication-to-github"
          },
          "meta": {
            "cicdSec": [
              2,
              7
            ],
            "plan": "enterprise-server",
            "planNote": "Only a setting on Enterprise Server; github.com has no password Git access."
          }
        },
        {
          "id": "auth-pat-expiry-policy",
          "text": "Set an organization-wide maximum PAT lifetime (90 days is a reasonable default)",
          "tier": "quick-win",
          "why": "A PAT that never expires is a permanent, unmonitored key sitting in whatever it was pasted into.",
          "prerequisites": [
            "Organization owner",
            "or Enterprise owner to set it for every org at once"
          ],
          "steps": [
            "Go to `github.com/organizations/ORG/settings/personal-access-tokens`.",
            "Open the \"Fine-grained tokens\" tab. Under \"Set maximum lifetimes for personal access tokens\", set 90 days. Save.",
            "Open the \"Tokens (classic)\" tab and set the same maximum lifetime. Save.",
            "Tokens with a longer lifetime are blocked from org resources, so warn automation owners before you save."
          ],
          "docs": {
            "title": "Setting a PAT policy for your organization",
            "url": "https://docs.github.com/en/organizations/managing-programmatic-access-to-your-organization/setting-a-personal-access-token-policy-for-your-organization"
          },
          "scenarioIds": [
            "user-mercedes-benz-token-leak"
          ],
          "meta": {
            "cicdSec": [
              6
            ],
            "plan": "free",
            "lastVerified": "2026-10-11"
          },
          "detect": {
            "events": [
              "personal_access_token.expiration_limit_unset",
              "personal_access_token.expiration_limit_set"
            ],
            "match": "exact"
          }
        },
        {
          "id": "auth-mfa-bots",
          "text": "Require MFA on bot/service accounts, not just human ones",
          "tier": "quick-win",
          "why": "Shared automation accounts are rarely rotated and make an attractive, under-monitored target.",
          "prerequisites": [
            "Organization owner (to see 2FA status)",
            "access to the bot account's login"
          ],
          "steps": [
            "Go to `github.com/orgs/ORG/people` and list every bot or shared account.",
            "Sign in as each one and go to `github.com/settings/security`. Enable 2FA with an authenticator app.",
            "Store the TOTP secret and recovery codes in your team's password vault, not with one person.",
            "Where the account only exists for automation, plan to replace it with a GitHub App instead."
          ],
          "docs": {
            "title": "Configuring two-factor authentication",
            "url": "https://docs.github.com/en/authentication/securing-your-account-with-two-factor-authentication-2fa/configuring-two-factor-authentication"
          },
          "scenarioIds": [
            "org-ghostaction"
          ],
          "meta": {
            "cicdSec": [
              2,
              6
            ],
            "plan": "free"
          },
          "detect": {
            "events": [
              "org.disable_two_factor_requirement",
              "business.disable_two_factor_requirement"
            ],
            "match": "related"
          }
        },
        {
          "id": "auth-audit-stale-creds",
          "text": "Audit and revoke stale PATs, unused SSH keys, and inactive accounts",
          "tier": "quarter",
          "why": "Stale credentials are exactly what attackers enumerate first. They're free to check, and they often still work.",
          "prerequisites": [
            "Organization owner",
            "Enterprise owner for the dormant users report"
          ],
          "steps": [
            "Go to `github.com/organizations/ORG/settings/personal-access-tokens/active`. Revoke fine-grained tokens that are unused or expired.",
            "If you use SAML SSO, open each member at `github.com/orgs/ORG/people` and review their authorized credentials. Revoke old PATs and SSH keys there.",
            "On the enterprise, go to Compliance → Reports → \"Dormant Users\" and download the report.",
            "Remove dormant members and outside collaborators from the org.",
            "In each repo, check `github.com/ORG/REPO/settings/keys` and delete deploy keys nobody owns."
          ],
          "docs": {
            "title": "Reviewing and revoking PATs in your organization",
            "url": "https://docs.github.com/en/organizations/managing-programmatic-access-to-your-organization/reviewing-and-revoking-personal-access-tokens-in-your-organization"
          },
          "scenarioIds": [
            "user-mercedes-benz-token-leak"
          ],
          "meta": {
            "cicdSec": [
              2,
              6
            ],
            "plan": "free",
            "planNote": "The dormant users report needs Enterprise."
          },
          "detect": {
            "events": [
              "public_key.create",
              "personal_access_token.access_granted",
              "org_credential_authorization.grant"
            ],
            "match": "related"
          }
        },
        {
          "id": "auth-disable-classic-pats",
          "text": "Disable classic PATs organization-wide, forcing migration to fine-grained PATs",
          "tier": "quarter",
          "why": "Classic PATs are all-or-nothing. One leaked token can mean full read-write to everything that user can touch.",
          "prerequisites": [
            "Organization owner"
          ],
          "steps": [
            "Tell token owners the date, and ask them to move to fine-grained PATs or a GitHub App.",
            "Go to `github.com/organizations/ORG/settings/personal-access-tokens`.",
            "Open the \"Tokens (classic)\" tab and select \"Restrict personal access tokens (classic) from accessing your organizations\". Save.",
            "On the \"Fine-grained tokens\" tab, turn on \"Require administrator approval\" so new tokens get reviewed."
          ],
          "docs": {
            "title": "Setting a PAT policy for your organization",
            "url": "https://docs.github.com/en/organizations/managing-programmatic-access-to-your-organization/setting-a-personal-access-token-policy-for-your-organization"
          },
          "scenarioIds": [
            "user-dependabot-impersonation"
          ],
          "meta": {
            "cicdSec": [
              6
            ],
            "plan": "free",
            "lastVerified": "2026-10-11"
          },
          "detect": {
            "events": [
              "personal_access_token.access_restriction_disabled",
              "auto_approve_personal_access_token_requests.enable"
            ],
            "match": "exact"
          }
        },
        {
          "id": "auth-saml-sso",
          "text": "Migrate to SAML SSO / Enterprise Managed Users for centralized identity control",
          "tier": "initiative",
          "why": "Centralizing identity means one enforced MFA/session policy instead of dozens of personal-account configurations, though the SSO layer itself then becomes worth protecting carefully.",
          "prerequisites": [
            "Organization owner or Enterprise owner",
            "admin on your identity provider"
          ],
          "steps": [
            "Create a GitHub app/connector in your IdP (Entra ID, Okta, etc.) and assign a test group.",
            "Go to `github.com/organizations/ORG/settings/security` (or `github.com/enterprises/ENTERPRISE/settings/security` for every org).",
            "Under \"SAML single sign-on\", enable it and paste the IdP sign-on URL, issuer and certificate.",
            "Click \"Test SAML configuration\". When it passes, select \"Require SAML SSO authentication\". Save.",
            "For Enterprise Managed Users, contact GitHub sales. EMU is a new enterprise, not a setting you can flip."
          ],
          "docs": {
            "title": "Enabling and testing SAML SSO for your organization",
            "url": "https://docs.github.com/en/enterprise-cloud@latest/organizations/managing-saml-single-sign-on-for-your-organization/enabling-and-testing-saml-single-sign-on-for-your-organization"
          },
          "scenarioIds": [
            "ent-cve-2024-4985"
          ],
          "meta": {
            "cicdSec": [
              2
            ],
            "plan": "enterprise"
          },
          "detect": {
            "events": [
              "org.disable_saml",
              "business.disable_saml",
              "business.disable_oidc",
              "org.update_saml_provider_settings"
            ],
            "match": "exact"
          }
        },
        {
          "id": "auth-idp-group-teams",
          "text": "Manage team membership from identity provider groups instead of by hand",
          "tier": "initiative",
          "kind": "coverage",
          "why": "When teams follow IdP groups, a leaver loses repo access the moment HR offboards them, not whenever someone remembers.",
          "prerequisites": [
            "SAML SSO or Enterprise Managed Users already set up",
            "Organization owner",
            "admin on your identity provider"
          ],
          "steps": [
            "Decide which IdP groups map to which GitHub teams, and grant repo access to those teams only.",
            "With Enterprise Managed Users, open each team at `github.com/orgs/ORG/teams` → Settings and connect it to an IdP group. Membership then comes from SCIM.",
            "With SAML SSO on a normal enterprise, turn on team synchronization at `github.com/organizations/ORG/settings/security`, then link each team to a group under the team's settings.",
            "Nested IdP groups are not expanded. Link the leaf groups, or flatten membership in the IdP or with Terraform.",
            "Remove the members you added by hand once each team is linked."
          ],
          "docs": {
            "title": "Managing team memberships with identity provider groups",
            "url": "https://docs.github.com/en/enterprise-cloud@latest/admin/managing-iam/provisioning-user-accounts-with-scim/managing-team-memberships-with-identity-provider-groups"
          },
          "meta": {
            "cicdSec": [
              2
            ],
            "plan": "enterprise",
            "planNote": "Enterprise Cloud with SAML SSO, or Enterprise Managed Users."
          },
          "detect": {
            "events": [
              "external_group.unlink",
              "team_sync_tenant.disabled",
              "team.add_member"
            ],
            "match": "exact"
          }
        }
      ]
    },
    {
      "category": "Organization governance",
      "items": [
        {
          "id": "org-base-permission-read",
          "text": "Set the organization's base repository permission to None or Read, not Write or Admin",
          "tier": "quick-win",
          "why": "A permissive org-wide default silently grants every member admin-like access to every repo at once.",
          "check": {
            "scope": "org",
            "calls": [
              "/orgs/{org}"
            ],
            "jq": ".[0].default_repository_permission | if . == null then null else (. == \"read\" or . == \"none\") end",
            "detail": "\"Base permission: \\(.[0].default_repository_permission // \"not visible\")\"",
            "needs": "Organization owner (classic token: admin:org, read:org)"
          },
          "prerequisites": [
            "Organization owner"
          ],
          "steps": [
            "Find out which teams need write access and grant it to those teams on the repos they work in.",
            "Go to `github.com/organizations/ORG/settings/member_privileges`.",
            "Under \"Base permissions\", select Read. Confirm the change.",
            "Choose No permission instead if access should come only from team grants. Members then see only public and internal repos and the repos their teams are given."
          ],
          "docs": {
            "title": "Setting base permissions for an organization",
            "url": "https://docs.github.com/en/organizations/managing-user-access-to-your-organizations-repositories/managing-repository-roles/setting-base-permissions-for-an-organization"
          },
          "scenarioIds": [
            "org-toptal"
          ],
          "meta": {
            "cicdSec": [
              2
            ],
            "plan": "free"
          },
          "detect": {
            "events": [
              "org.update_default_repository_permission",
              "business.update_default_repository_permission"
            ],
            "match": "exact"
          }
        },
        {
          "id": "org-limit-owners",
          "text": "Limit the number of organization owners to the minimum needed",
          "tier": "quick-win",
          "why": "Every additional owner is another full-organization compromise waiting to happen.",
          "check": {
            "scope": "org",
            "calls": [
              "/orgs/{org}/members?role=admin&per_page=100"
            ],
            "jq": "(.[0] | length) <= 3",
            "detail": "\"\\(.[0] | length) owners: \\(.[0] | map(.login) | join(\", \"))\"",
            "needs": "Organization member (classic token: read:org)",
            "note": "Passes at 3 owners or fewer."
          },
          "prerequisites": [
            "Organization owner"
          ],
          "steps": [
            "Go to `github.com/orgs/ORG/people?query=role%3Aowner`.",
            "For each owner who doesn't need full control, select them and use \"Change role\" to set Member.",
            "Keep two or three owners so you never lose ownership of the org.",
            "Give people who only need security access the Security manager role instead."
          ],
          "docs": {
            "title": "Maintaining ownership continuity for your organization",
            "url": "https://docs.github.com/en/organizations/managing-peoples-access-to-your-organization-with-roles/maintaining-ownership-continuity-for-your-organization"
          },
          "scenarioIds": [
            "org-toptal"
          ],
          "meta": {
            "cicdSec": [
              2
            ],
            "plan": "free"
          },
          "detect": {
            "events": [
              "org.update_member",
              "business.add_admin",
              "business.invite_admin"
            ],
            "match": "exact"
          }
        },
        {
          "id": "org-restrict-member-privileges",
          "text": "Restrict public repo creation, visibility changes, repo deletion, private forks and outside collaborator invites to owners",
          "tier": "quick-win",
          "why": "Any member who can make a repo public or invite an outsider can leak private code without touching a single permission.",
          "check": {
            "scope": "org",
            "calls": [
              "/orgs/{org}"
            ],
            "jq": ".[0] | if .members_can_create_public_repositories == null then null else (.members_can_create_public_repositories == false and .members_can_fork_private_repositories != true) end",
            "detail": ".[0] | [\"members_can_create_public_repositories\", \"members_can_fork_private_repositories\"] as $k | [$k[] as $f | \"\\($f): \\(if .[$f] == null then \"not visible\" else .[$f] end)\"] | join(\", \")",
            "needs": "Organization owner (classic token: admin:org, read:org)",
            "note": "The API only shows public repo creation and private forking. Check visibility changes, deletion and invites by hand."
          },
          "prerequisites": [
            "Organization owner",
            "or Enterprise owner to enforce it for every org"
          ],
          "steps": [
            "Go to `github.com/organizations/ORG/settings/member_privileges`.",
            "Under \"Repository creation\", untick Public so members can only create private or internal repos.",
            "Under \"Repository forking\", untick \"Allow forking of private and internal repositories\" unless teams need it.",
            "Under \"Repository visibility change\" and \"Repository deletion and transfer\", untick the options that let members do it. Only owners can then change visibility or delete a repo.",
            "Under \"Repository invitations\", untick \"Allow members to invite outside collaborators\".",
            "On an enterprise, set the same policies at `github.com/enterprises/ENTERPRISE/settings/member_privileges` so no org can loosen them."
          ],
          "docs": {
            "title": "Restricting repository visibility changes in your organization",
            "url": "https://docs.github.com/en/organizations/managing-organization-settings/restricting-repository-visibility-changes-in-your-organization"
          },
          "scenarioIds": [
            "org-toptal",
            "org-shai-hulud"
          ],
          "meta": {
            "cicdSec": [
              2,
              7
            ],
            "plan": "free",
            "planNote": "Limiting outside collaborator invites to owners needs Enterprise Cloud."
          },
          "detect": {
            "events": [
              "org.update_member_repository_creation_permission",
              "org.update_member_repository_invitation_permission",
              "repository_visibility_change.enable",
              "private_repository_forking.enable"
            ],
            "match": "exact"
          }
        },
        {
          "id": "org-custom-roles",
          "text": "Use custom organization and repository roles instead of handing out Owner or Admin",
          "tier": "quarter",
          "why": "Without a narrower role, the security lead or release manager ends up with Owner or Admin because nothing smaller fits.",
          "check": {
            "scope": "org",
            "calls": [
              "/orgs/{org}/organization-roles",
              "/orgs/{org}/custom-repository-roles"
            ],
            "jq": "null",
            "detail": "\"Org roles: \\((.[0].roles // []) | map(select(.source != \"Predefined\") | .name) | if length == 0 then \"no custom roles\" else join(\", \") end); custom repo roles: \\((.[1].custom_roles // []) | map(.name) | if length == 0 then \"none\" else join(\", \") end)\"",
            "needs": "Organization owner (classic token: admin:org, read:org)",
            "note": "Lists the roles; whether they fit your teams is your call."
          },
          "prerequisites": [
            "Organization owner"
          ],
          "steps": [
            "List what the people with Owner or repo Admin actually do, for example review security alerts, manage releases or edit webhooks.",
            "Go to `github.com/organizations/ORG/settings/org_roles` and create org roles for those jobs, or assign a predefined one such as Security manager or CI/CD admin.",
            "Go to `github.com/organizations/ORG/settings/roles` and create repository roles based on Write or Maintain, adding only the extra permissions needed.",
            "Assign the roles to teams, then remove the Owner and Admin grants they replace."
          ],
          "docs": {
            "title": "About custom repository roles",
            "url": "https://docs.github.com/en/enterprise-cloud@latest/organizations/managing-user-access-to-your-organizations-repositories/managing-repository-roles/about-custom-repository-roles"
          },
          "scenarioIds": [
            "org-toptal"
          ],
          "meta": {
            "cicdSec": [
              2
            ],
            "plan": "enterprise",
            "planNote": "Custom repository roles need Enterprise Cloud."
          },
          "detect": {
            "events": [
              "org.update_member",
              "organization_role.assign",
              "organization_role.update",
              "role.update"
            ],
            "match": "related"
          }
        },
        {
          "id": "org-access-review",
          "text": "Run a quarterly access review of owners, repo admins, team membership and outside collaborators",
          "tier": "quarter",
          "why": "Access piles up with every project and reorg, and nothing in GitHub removes it for you.",
          "prerequisites": [
            "Organization owner",
            "Enterprise owner for enterprise-wide exports"
          ],
          "steps": [
            "Export members with their roles from `github.com/orgs/ORG/people` (Export → CSV or JSON).",
            "List outside collaborators at `github.com/orgs/ORG/outside-collaborators` and check each still has a reason and an internal sponsor.",
            "For your most sensitive repos, review `github.com/ORG/REPO/settings/access` for direct Admin grants that bypass teams.",
            "Send each team's member list to its maintainer to confirm, and remove what they don't sign off on.",
            "Keep the exports and sign-offs as audit evidence."
          ],
          "docs": {
            "title": "Exporting member information for your organization",
            "url": "https://docs.github.com/en/enterprise-cloud@latest/organizations/managing-membership-in-your-organization/exporting-member-information-for-your-organization"
          },
          "scenarioIds": [
            "user-mercedes-benz-token-leak"
          ],
          "meta": {
            "cicdSec": [
              2
            ],
            "plan": "free"
          },
          "detect": {
            "events": [
              "org.update_member",
              "org.add_outside_collaborator",
              "repo.add_member",
              "team.add_member"
            ],
            "match": "related"
          }
        },
        {
          "id": "org-custom-properties",
          "text": "Classify repos with custom properties (data classification, criticality, compliance scope)",
          "tier": "quarter",
          "why": "Once repos carry a classification, rulesets and security configurations can target the critical ones automatically instead of by a hand-kept list.",
          "check": {
            "scope": "org",
            "calls": [
              "/orgs/{org}/properties/schema"
            ],
            "jq": "(.[0] // []) | length > 0",
            "detail": "\"Custom properties: \\((.[0] // []) | map(.property_name) | if length == 0 then \"none\" else join(\", \") end)\"",
            "needs": "Organization member (classic token: read:org)",
            "note": "Passes when at least one property is defined. Whether repos have values set is not checked."
          },
          "prerequisites": [
            "Organization owner",
            "or a custom role with permission to edit custom properties"
          ],
          "steps": [
            "Go to `github.com/organizations/ORG/settings/custom-properties`.",
            "Add properties such as `data-classification` (public, internal, confidential, restricted) and `criticality` (critical, high, medium, low). Make them required with a default.",
            "Set values per repo, or in bulk from the repository list on the same page.",
            "In your rulesets at `github.com/organizations/ORG/settings/rules`, target repositories by property, for example stricter review on `criticality: critical`.",
            "Apply security configurations to repos filtered by the same properties."
          ],
          "docs": {
            "title": "Managing custom properties for repositories",
            "url": "https://docs.github.com/en/organizations/managing-organization-settings/managing-custom-properties-for-repositories-in-your-organization"
          },
          "meta": {
            "cicdSec": [
              7
            ],
            "plan": "free"
          },
          "detect": {
            "events": [
              "custom_property_definition.destroy",
              "custom_property_definition.update",
              "custom_property_value.update",
              "custom_property_value.destroy"
            ],
            "match": "exact"
          }
        },
        {
          "id": "org-review-installed-apps",
          "text": "Review every currently installed GitHub App / OAuth App and its granted scope",
          "tier": "quarter",
          "why": "An installed app with broad scope is a standing credential that nobody is actively watching.",
          "check": {
            "scope": "org",
            "calls": [
              "/orgs/{org}/installations?per_page=100"
            ],
            "jq": "null",
            "detail": "\"\\(.[0].installations | length) apps installed: \\(.[0].installations | map(\"\\(.app_slug) (\\(.repository_selection))\") | join(\", \"))\"",
            "needs": "Organization owner (classic token: admin:org, read:org)",
            "note": "Lists the apps; whether each one should be there is your call."
          },
          "prerequisites": [
            "Organization owner"
          ],
          "steps": [
            "Go to `github.com/organizations/ORG/settings/installations`.",
            "Click Configure on each app. Check its permissions and which repositories it can access.",
            "Narrow repository access to \"Only select repositories\", or uninstall apps nobody uses.",
            "Go to `github.com/organizations/ORG/settings/oauth_application_policy` and remove approval for OAuth apps you don't recognize."
          ],
          "docs": {
            "title": "Reviewing GitHub Apps installed in your organization",
            "url": "https://docs.github.com/en/organizations/managing-programmatic-access-to-your-organization/reviewing-github-apps-installed-in-your-organization"
          },
          "scenarioIds": [
            "app-heroku-travisci-oauth-theft"
          ],
          "meta": {
            "cicdSec": [
              8
            ],
            "plan": "free"
          },
          "detect": {
            "events": [
              "integration_installation.create",
              "integration_installation.version_updated",
              "integration_installation.repositories_added",
              "org.oauth_app_access_approved"
            ],
            "match": "exact"
          }
        },
        {
          "id": "org-approval-for-new-apps",
          "text": "Keep OAuth app access restrictions on and require security/IT approval for new apps",
          "tier": "quarter",
          "why": "Unreviewed app installs are how a single click grants a third party lasting access to org data.",
          "prerequisites": [
            "Organization owner"
          ],
          "steps": [
            "Go to `github.com/organizations/ORG/settings/oauth_application_policy`.",
            "Click \"Setup application access restrictions\", then \"Restrict third-party application access\".",
            "Only owners can install GitHub Apps. Members can only send a request, so keep the owner list short.",
            "Route every app request (they arrive by email to owners) through a security ticket before approving it."
          ],
          "docs": {
            "title": "Enabling OAuth app access restrictions",
            "url": "https://docs.github.com/en/organizations/managing-oauth-access-to-your-organizations-data/enabling-oauth-app-access-restrictions-for-your-organization"
          },
          "scenarioIds": [
            "user-oauth-consent-phishing"
          ],
          "meta": {
            "cicdSec": [
              8
            ],
            "plan": "free",
            "lastVerified": "2026-10-11"
          },
          "detect": {
            "events": [
              "org.disable_oauth_app_restrictions",
              "integration_installation.create",
              "org.integration_manager_added"
            ],
            "match": "related"
          }
        },
        {
          "id": "org-audit-log-siem",
          "text": "Enable audit logging and export it to a SIEM for retention and detection",
          "tier": "quarter",
          "why": "Without exported logs, enumeration and quiet persistence leave no trace to investigate later.",
          "prerequisites": [
            "Enterprise owner",
            "a SIEM endpoint (Splunk, Datadog, S3, Azure, etc.)"
          ],
          "steps": [
            "Go to `github.com/enterprises/ENTERPRISE` → Settings → Audit log → Log streaming.",
            "Click \"Configure stream\", pick your SIEM, and enter its endpoint and credentials.",
            "Click \"Check endpoint\", then Save.",
            "On the same page, turn on streaming of API request events so token use is logged too.",
            "Build alerts for owner changes, new app installs, PAT creation, secret or ruleset changes, repo deletion and repos made public."
          ],
          "docs": {
            "title": "Streaming the audit log for your enterprise",
            "url": "https://docs.github.com/en/enterprise-cloud@latest/admin/monitoring-activity-in-your-enterprise/reviewing-audit-logs-for-your-enterprise/streaming-the-audit-log-for-your-enterprise"
          },
          "scenarioIds": [
            "org-salesloft-drift"
          ],
          "meta": {
            "cicdSec": [
              10
            ],
            "plan": "enterprise"
          },
          "detect": {
            "events": [
              "audit_log_streaming.destroy",
              "audit_log_streaming.update"
            ],
            "match": "exact"
          }
        },
        {
          "id": "org-ip-allowlist",
          "text": "Enforce an IP allow list for Git and web UI access from trusted ranges",
          "tier": "quarter",
          "why": "A network boundary stops a stolen credential from being usable from outside your own infrastructure.",
          "prerequisites": [
            "Organization owner"
          ],
          "steps": [
            "Collect every range that needs access: office, VPN, self-hosted runners, and external services that call GitHub.",
            "Go to `github.com/organizations/ORG/settings/security`.",
            "Under \"IP allow list\", add each range with a description.",
            "Select \"Enable IP allow list\". Save.",
            "Also enable the allow list for installed GitHub Apps if your apps publish their ranges."
          ],
          "docs": {
            "title": "Managing allowed IP addresses for your organization",
            "url": "https://docs.github.com/en/enterprise-cloud@latest/organizations/keeping-your-organization-secure/managing-security-settings-for-your-organization/managing-allowed-ip-addresses-for-your-organization"
          },
          "meta": {
            "cicdSec": [
              7
            ],
            "plan": "enterprise"
          },
          "detect": {
            "events": [
              "ip_allow_list.disable",
              "ip_allow_list_entry.create",
              "ip_allow_list_entry.update",
              "ip_allow_list.disable_for_installed_apps"
            ],
            "match": "exact"
          }
        },
        {
          "id": "org-allowed-apps-doc",
          "text": "Maintain a documented list of allowed apps, their use case, and their owner",
          "tier": "quarter",
          "why": "An undocumented app estate can't be reviewed, audited, or confidently revoked later.",
          "prerequisites": [
            "Organization owner (or a token with admin:org read)"
          ],
          "steps": [
            "Export the installed apps from `github.com/organizations/ORG/settings/installations` or `gh api /orgs/ORG/installations`.",
            "For each app, write down: what it's for, the business owner, its permissions, and which repos it can reach.",
            "Keep the list in a repo so changes get reviewed.",
            "Put a quarterly review on the calendar and remove apps that no longer have an owner."
          ],
          "docs": {
            "title": "REST API: list app installations for an organization",
            "url": "https://docs.github.com/en/rest/orgs/orgs#list-app-installations-for-an-organization"
          },
          "meta": {
            "cicdSec": [
              8
            ],
            "plan": "free"
          },
          "detect": {
            "events": [
              "integration_installation.create",
              "org.oauth_app_access_approved"
            ],
            "match": "related"
          }
        },
        {
          "id": "org-agent-mcp-controls",
          "text": "Decide which AI coding agents and MCP servers may work on the organization's repositories",
          "tier": "quarter",
          "why": "An agent acts with the access it is given and reads untrusted text (issues, pull requests, web pages), so a prompt injection can turn that access against you.",
          "prerequisites": [
            "Organization owner",
            "or Enterprise owner, where the enterprise sets the policy"
          ],
          "steps": [
            "In the organization settings, open the Copilot policies and review the agent ones: the Copilot coding agent, third-party coding agents and MCP servers in Copilot.",
            "Turn agents on only for the repositories that need them, not the whole organization.",
            "Keep agent work behind pull requests with required human review, and do not give agents environment secrets for production.",
            "Limit which MCP servers developers can use. GitHub recommends defining that in the enterprise's managed settings rather than with the registry preview."
          ],
          "docs": {
            "title": "Managing policies for Copilot in your organization",
            "url": "https://docs.github.com/en/copilot/how-tos/administer-copilot/manage-for-organization/manage-policies"
          },
          "scenarioIds": [
            "app-agentic-mcp-tokens",
            "org-nx-s1ngularity"
          ],
          "meta": {
            "cicdSec": [
              8
            ],
            "plan": "free",
            "planNote": "Needs a Copilot plan for the Copilot agent policies.",
            "lastVerified": "2026-10-11"
          },
          "detect": {
            "events": [
              "copilot.cfb_org_settings_changed",
              "copilot.cfb_enterprise_settings_changed",
              "mcp_registry.settings_update",
              "mcp_registry.allowlist_add_server"
            ],
            "match": "related"
          }
        },
        {
          "id": "org-backup-restore",
          "text": "Back up repositories and metadata outside GitHub, and rehearse a restore",
          "tier": "initiative",
          "why": "An attacker with owner or admin access can delete or wipe repositories. A deleted repository can usually be restored within 90 days, but not always, and force-pushed history and settings are not covered.",
          "prerequisites": [
            "Organization owner",
            "a storage location outside GitHub"
          ],
          "steps": [
            "Back up every repository on a schedule with Git (`git clone --mirror`) or the migrations API, to storage the GitHub credentials cannot delete.",
            "Include what Git does not hold: issues, pull requests, releases, wikis, and the repository and organization settings you would need to rebuild.",
            "Write down how to restore a deleted repository (organization settings, Deleted repositories, within 90 days) and when you need GitHub Support instead, for example for a repository in a fork network.",
            "Rehearse restoring one repository from your own backup once a year."
          ],
          "docs": {
            "title": "Backing up a repository",
            "url": "https://docs.github.com/en/repositories/archiving-a-github-repository/backing-up-a-repository"
          },
          "scenarioIds": [
            "org-toptal"
          ],
          "meta": {
            "cicdSec": [
              7
            ],
            "plan": "free",
            "lastVerified": "2026-10-11"
          },
          "detect": {
            "events": [
              "repo.destroy",
              "repo.transfer",
              "org.delete"
            ],
            "match": "related"
          }
        }
      ]
    },
    {
      "category": "Repository security",
      "items": [
        {
          "id": "repo-branch-protection",
          "text": "Branch protection on default/release branches: required reviews + required status checks",
          "tier": "quick-win",
          "kind": "coverage",
          "why": "Without required review, one push is all it takes to land malicious code on the branch everyone trusts.",
          "check": {
            "scope": "repo",
            "calls": [
              "/repos/{owner}/{repo}/rules/branches/{default_branch}",
              "/repos/{owner}/{repo}/branches/{default_branch}/protection"
            ],
            "jq": "(((.[0] // []) | any(.type == \"pull_request\")) or (.[1].required_pull_request_reviews != null)) and (((.[0] // []) | any(.type == \"required_status_checks\")) or (.[1].required_status_checks != null))",
            "needs": "Organization owner, or admin on every repo (classic token: repo, read:org)",
            "note": "Default branch only. Counts both rulesets and classic branch protection."
          },
          "prerequisites": [
            "Organization owner for org-wide rulesets",
            "or repo admin for one repo"
          ],
          "steps": [
            "Go to `github.com/organizations/ORG/settings/rules` (or `github.com/ORG/REPO/settings/rules` for one repo) and click \"New ruleset\" → \"New branch ruleset\".",
            "Set enforcement to Active. Target all repositories (or a pilot set) and the default branch.",
            "Turn on \"Require a pull request before merging\" with at least 1 approval and dismiss stale approvals.",
            "Turn on \"Require status checks to pass\" and add your CI checks. Also require branches to be up to date before merging.",
            "For repos that ship to production, raise the approval count to 2.",
            "Save, then widen the repository target as teams are ready."
          ],
          "docs": {
            "title": "Creating rulesets for repositories in your organization",
            "url": "https://docs.github.com/en/organizations/managing-organization-settings/creating-rulesets-for-repositories-in-your-organization"
          },
          "scenarioIds": [
            "org-ultralytics",
            "org-chaindrop"
          ],
          "meta": {
            "cicdSec": [
              1
            ],
            "plan": "team",
            "planNote": "Free on public repositories."
          },
          "detect": {
            "events": [
              "protected_branch.destroy",
              "protected_branch.update_pull_request_reviews_enforcement_level",
              "protected_branch.update_required_status_checks_enforcement_level",
              "protected_branch.policy_override"
            ],
            "match": "exact"
          }
        },
        {
          "id": "repo-secret-scanning",
          "text": "Secret scanning and push protection enabled",
          "tier": "quick-win",
          "kind": "coverage",
          "why": "A secret committed once is compromised forever. Scanning catches it before or as it lands.",
          "check": {
            "scope": "repo",
            "calls": [],
            "jq": "$repo.security_and_analysis as $s | if $s == null then null else ($s.secret_scanning.status == \"enabled\" and $s.secret_scanning_push_protection.status == \"enabled\") end",
            "needs": "Organization owner, or admin on every repo (classic token: repo, read:org)",
            "note": "Needs both secret scanning and push protection on."
          },
          "prerequisites": [
            "Organization owner or Security manager"
          ],
          "steps": [
            "Go to `github.com/organizations/ORG/settings/security_products`.",
            "Apply the GitHub recommended configuration, or create one with Secret scanning and Push protection enabled.",
            "Apply it to all repositories.",
            "Set it as the default for new repositories."
          ],
          "docs": {
            "title": "Applying a security configuration",
            "url": "https://docs.github.com/en/code-security/how-tos/secure-at-scale/configure-organization-security/establish-complete-coverage/apply-custom-configuration"
          },
          "scenarioIds": [
            "org-ghostaction"
          ],
          "meta": {
            "cicdSec": [
              6
            ],
            "plan": "team",
            "addon": "secret-protection",
            "planNote": "Free on public repositories."
          },
          "detect": {
            "events": [
              "repository_secret_scanning.disable",
              "repository_secret_scanning_push_protection.disable",
              "org.secret_scanning_push_protection_disable",
              "secret_scanning_push_protection.bypass"
            ],
            "match": "exact"
          }
        },
        {
          "id": "repo-dependabot",
          "text": "Dependency graph, Dependabot alerts and Dependabot security updates enabled",
          "tier": "quick-win",
          "kind": "coverage",
          "why": "A known-vulnerable dependency is the cheapest way in, and Dependabot is free on every repo.",
          "check": {
            "scope": "repo",
            "calls": [],
            "jq": "$repo.security_and_analysis as $s | if $s == null or $s.dependabot_security_updates == null then null else $s.dependabot_security_updates.status == \"enabled\" end",
            "needs": "Organization owner, or admin on every repo (classic token: repo, read:org)",
            "note": "Reads Dependabot security updates, which cannot be on without alerts and the dependency graph."
          },
          "prerequisites": [
            "Organization owner or Security manager"
          ],
          "steps": [
            "Go to `github.com/organizations/ORG/settings/security_products` and edit the configuration you apply to all repos.",
            "Turn on Dependency graph, Dependabot alerts and Dependabot security updates.",
            "Apply it to all repositories and set it as the default for new ones.",
            "Review open alerts at `github.com/orgs/ORG/security/alerts/dependabot`, starting with critical ones."
          ],
          "docs": {
            "title": "Configuring Dependabot security updates",
            "url": "https://docs.github.com/en/code-security/how-tos/secure-your-supply-chain/secure-your-dependencies/configure-security-updates"
          },
          "meta": {
            "cicdSec": [
              3
            ],
            "plan": "free"
          },
          "detect": {
            "events": [
              "dependency_graph.disable",
              "dependabot_alerts.disable",
              "repository_vulnerability_alerts.disable",
              "repository_security_updates.disable"
            ],
            "match": "exact"
          }
        },
        {
          "id": "repo-prevent-branch-deletion",
          "text": "Deletion and force-pushes prevented on critical branches",
          "tier": "quick-win",
          "kind": "coverage",
          "why": "Attackers reliably delete branches and rewrite history to erase evidence during cleanup.",
          "check": {
            "scope": "repo",
            "calls": [
              "/repos/{owner}/{repo}/rules/branches/{default_branch}",
              "/repos/{owner}/{repo}/branches/{default_branch}/protection"
            ],
            "jq": "(((.[0] // []) | any(.type == \"deletion\")) or (.[1].allow_deletions.enabled == false)) and (((.[0] // []) | any(.type == \"non_fast_forward\")) or (.[1].allow_force_pushes.enabled == false))",
            "needs": "Organization owner, or admin on every repo (classic token: repo, read:org)",
            "note": "Default branch only."
          },
          "prerequisites": [
            "Organization owner for org-wide rulesets",
            "or repo admin for one repo"
          ],
          "steps": [
            "Go to `github.com/organizations/ORG/settings/rules` and open your default-branch ruleset (or create one).",
            "Make sure \"Restrict deletions\" and \"Block force pushes\" are both on.",
            "Add release branches (for example `release/*`) to the target.",
            "Keep the bypass list empty, or limited to a break-glass team."
          ],
          "docs": {
            "title": "Available rules for rulesets",
            "url": "https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/available-rules-for-rulesets"
          },
          "scenarioIds": [
            "org-shai-hulud",
            "user-glassworm"
          ],
          "meta": {
            "cicdSec": [
              1
            ],
            "plan": "team",
            "planNote": "Free on public repositories."
          },
          "detect": {
            "events": [
              "protected_branch.update_allow_force_pushes_enforcement_level",
              "protected_branch.destroy",
              "repository_ruleset.update",
              "protected_branch.policy_override"
            ],
            "match": "exact"
          }
        },
        {
          "id": "repo-security-md",
          "text": "Add a .github/SECURITY.md with a disclosure contact and process",
          "tier": "quick-win",
          "why": "Without a disclosure path, a researcher's report is just as likely to go nowhere as to reach you.",
          "check": {
            "scope": "org",
            "calls": [
              "/repos/{org}/.github/contents/SECURITY.md",
              "/repos/{org}/.github/contents/.github/SECURITY.md",
              "/repos/{org}/.github/contents/docs/SECURITY.md"
            ],
            "jq": "any(.[]; . != null)",
            "detail": "if any(.[]; . != null) then \"Found an org-wide SECURITY.md in the .github repo\" else \"No SECURITY.md in the org .github repo\" end",
            "needs": "Read access to the org's .github repo",
            "note": "Checks the org-wide default only. Repos with their own SECURITY.md are not counted."
          },
          "prerequisites": [
            "Permission to create repositories in the org"
          ],
          "steps": [
            "Create a public repo named `.github` at `github.com/organizations/ORG/repositories/new`.",
            "Add `SECURITY.md` at its root with a contact address, supported versions and expected response time. It applies to every repo that has no policy of its own.",
            "Turn on private vulnerability reporting at `github.com/organizations/ORG/settings/security_products` so reports arrive as private advisories."
          ],
          "docs": {
            "title": "Adding a security policy to your repository",
            "url": "https://docs.github.com/en/code-security/how-tos/report-and-fix-vulnerabilities/configure-vulnerability-reporting/add-security-policy"
          },
          "meta": {
            "cicdSec": [
              10
            ],
            "plan": "free"
          }
        },
        {
          "id": "repo-codeowners",
          "text": "CODEOWNERS in place, required on sensitive paths",
          "tier": "quarter",
          "kind": "coverage",
          "why": "No enforced review on sensitive paths means anyone with write access can change them unseen.",
          "check": {
            "scope": "repo",
            "calls": [
              "/repos/{owner}/{repo}/codeowners/errors",
              "/repos/{owner}/{repo}/rules/branches/{default_branch}",
              "/repos/{owner}/{repo}/branches/{default_branch}/protection"
            ],
            "jq": "(.[0] != null and (.[0].errors | length) == 0) and (((.[1] // []) | any(.type == \"pull_request\" and .parameters.require_code_owner_review == true)) or (.[2].required_pull_request_reviews.require_code_owner_reviews == true))",
            "needs": "Organization owner, or admin on every repo (classic token: repo, read:org)",
            "note": "Needs a valid CODEOWNERS file and code owner review required on the default branch."
          },
          "prerequisites": [
            "Write access to the repo",
            "owners must be teams or users with write access",
            "Organization owner or repo admin to change rulesets"
          ],
          "steps": [
            "Add `.github/CODEOWNERS` mapping sensitive paths to teams, for example `/.github/workflows/ @ORG/platform-security`.",
            "Cover at least workflows, infra code, auth code and the CODEOWNERS file itself.",
            "In your branch ruleset at `github.com/organizations/ORG/settings/rules`, turn on \"Require review from Code Owners\".",
            "Open a test PR that touches a covered path and check the right team is requested."
          ],
          "docs": {
            "title": "About code owners",
            "url": "https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners"
          },
          "scenarioIds": [
            "org-nx-s1ngularity"
          ],
          "meta": {
            "cicdSec": [
              1
            ],
            "plan": "team",
            "planNote": "Free on public repositories."
          },
          "detect": {
            "events": [
              "protected_branch.update_require_code_owner_review",
              "repository_ruleset.update"
            ],
            "match": "exact"
          }
        },
        {
          "id": "repo-rulesets",
          "text": "Rulesets applied: push restriction, tag protection, sensitive file-path protection",
          "tier": "quarter",
          "kind": "coverage",
          "why": "Tag protection is what stops a trusted version from being silently repointed after release.",
          "check": {
            "scope": "org",
            "calls": [
              "/orgs/{org}/rulesets?per_page=100"
            ],
            "jq": "[(.[0] | any(.target == \"tag\" and .enforcement == \"active\")), (.[0] | any(.target == \"push\" and .enforcement == \"active\"))] | map(select(.)) | length * 50",
            "detail": "\"Active org rulesets: \\(.[0] | map(select(.enforcement == \"active\")) | map(\"\\(.name) (\\(.target))\") | join(\", \") | if . == \"\" then \"none\" else . end)\"",
            "needs": "Organization owner (classic token: admin:org, read:org)",
            "note": "50% for an active tag ruleset, 50% for an active push ruleset. Repo-level rulesets are not counted."
          },
          "prerequisites": [
            "Organization owner"
          ],
          "steps": [
            "Go to `github.com/organizations/ORG/settings/rules`.",
            "New ruleset → \"New tag ruleset\". Target `v*`. Turn on \"Restrict updates\" and \"Restrict deletions\". Put your release automation on the bypass list only.",
            "New ruleset → \"New push ruleset\". Restrict file paths such as `.github/workflows/**` so only the bypass team can push changes there.",
            "On Enterprise, start each ruleset in Evaluate mode, review Rule insights for a week, then switch to Active."
          ],
          "docs": {
            "title": "Available rules for rulesets",
            "url": "https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/available-rules-for-rulesets"
          },
          "scenarioIds": [
            "app-tj-actions-changed-files",
            "org-teampcp",
            "org-chaindrop"
          ],
          "meta": {
            "cicdSec": [
              1
            ],
            "plan": "team",
            "planNote": "Free on public repositories; push rulesets need private or internal repositories."
          },
          "detect": {
            "events": [
              "repository_ruleset.destroy",
              "repository_ruleset.update"
            ],
            "match": "exact"
          }
        },
        {
          "id": "repo-signed-commits",
          "text": "Require signed commits on the default branch of sensitive repos",
          "tier": "quarter",
          "kind": "coverage",
          "why": "Git trusts whatever name and email a commit claims, so without signatures a commit can pose as any developer.",
          "check": {
            "scope": "repo",
            "calls": [
              "/repos/{owner}/{repo}/rules/branches/{default_branch}",
              "/repos/{owner}/{repo}/branches/{default_branch}/protection"
            ],
            "jq": "((.[0] // []) | any(.type == \"required_signatures\")) or (.[1].required_signatures.enabled == true)",
            "needs": "Organization owner, or admin on every repo (classic token: repo, read:org)",
            "note": "Default branch only. Counts every repo, so a low score is expected if you only require it on sensitive ones."
          },
          "prerequisites": [
            "Organization owner for org-wide rulesets",
            "or repo admin for one repo"
          ],
          "steps": [
            "Ask developers to set up SSH or GPG commit signing and upload the key at `github.com/settings/keys` as a signing key.",
            "Go to `github.com/organizations/ORG/settings/rules` and open the ruleset for your sensitive repos (a custom property target works well here).",
            "Turn on \"Require signed commits\". Start in Evaluate mode to see who would be blocked.",
            "Commits made in the web UI and by GitHub Apps are signed by GitHub, so merge queues and bots keep working. Check other automation that pushes directly."
          ],
          "docs": {
            "title": "About commit signature verification",
            "url": "https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification"
          },
          "meta": {
            "cicdSec": [
              1,
              9
            ],
            "plan": "team",
            "planNote": "Free on public repositories."
          },
          "detect": {
            "events": [
              "protected_branch.update_signature_requirement_enforcement_level",
              "repository_ruleset.update"
            ],
            "match": "exact"
          }
        },
        {
          "id": "repo-artifact-attestation",
          "text": "Adopt artifact attestations for build provenance on published binaries/images",
          "tier": "initiative",
          "why": "Provenance is the only way to prove what actually built an artifact, after the fact.",
          "prerequisites": [
            "Write access to the workflow"
          ],
          "steps": [
            "In the release workflow, add `permissions:` with `id-token: write`, `attestations: write` and `contents: read`.",
            "After the build step, add `actions/attest-build-provenance` with `subject-path` pointing at the built file (or `subject-name` and `subject-digest` for an image).",
            "Run a release and check the attestation at `github.com/ORG/REPO/attestations`.",
            "To ship an SBOM with the release, generate one (for example with `anchore/sbom-action`) and attest it with `actions/attest-sbom`.",
            "Verify it where the artifact is consumed: `gh attestation verify FILE -R ORG/REPO`."
          ],
          "docs": {
            "title": "Using artifact attestations",
            "url": "https://docs.github.com/en/actions/how-tos/secure-your-work/use-artifact-attestations/use-artifact-attestations"
          },
          "meta": {
            "cicdSec": [
              9
            ],
            "plan": "enterprise",
            "planNote": "Public repositories on any plan."
          }
        },
        {
          "id": "repo-immutable-releases",
          "text": "Make releases immutable, or protect release tags with a tag ruleset",
          "tier": "quick-win",
          "kind": "coverage",
          "why": "If a release tag or its assets can be changed after publication, an attacker with write access can point everyone who trusted that version at new code.",
          "prerequisites": [
            "Organization owner, or admin on the repository"
          ],
          "steps": [
            "Turn on immutable releases for the organization or for each repository that publishes releases, so a published release's assets and tag cannot change.",
            "For tags without releases, add a tag ruleset on `v*` (or your release pattern) that blocks updates and deletion, with an empty bypass list.",
            "Publish Actions and packages only from these tags, and tell consumers to pin by commit SHA."
          ],
          "docs": {
            "title": "Immutable releases",
            "url": "https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/immutable-releases"
          },
          "scenarioIds": [
            "app-tj-actions-changed-files",
            "user-glassworm",
            "org-chaindrop"
          ],
          "meta": {
            "cicdSec": [
              9
            ],
            "plan": "free",
            "planNote": "Tag rulesets on private repositories need Team.",
            "lastVerified": "2026-10-11"
          },
          "detect": {
            "events": [
              "repo.immutable_releases_settings_disabled",
              "org.update_immutable_releases_settings_policy",
              "repository_ruleset.update",
              "repository_ruleset.destroy"
            ],
            "match": "exact"
          }
        }
      ]
    },
    {
      "category": "CI/CD & GitHub Actions",
      "items": [
        {
          "id": "cicd-default-permissions-read",
          "text": "Set the default GITHUB_TOKEN permission to read-only at the org level",
          "tier": "quick-win",
          "why": "A permissive default token hands write access to every workflow, including third-party actions it calls.",
          "check": {
            "scope": "org",
            "calls": [
              "/orgs/{org}/actions/permissions/workflow"
            ],
            "jq": ".[0].default_workflow_permissions == \"read\"",
            "detail": "\"default_workflow_permissions: \\(.[0].default_workflow_permissions)\"",
            "needs": "Organization owner (classic token: admin:org, read:org)"
          },
          "prerequisites": [
            "Organization owner",
            "or Enterprise owner to enforce it for all orgs"
          ],
          "steps": [
            "Go to `github.com/organizations/ORG/settings/actions`.",
            "Under \"Workflow permissions\", select \"Read repository contents and packages permissions\". Save.",
            "Workflows that need more add an explicit `permissions:` block with only the scopes they use."
          ],
          "docs": {
            "title": "Setting GITHUB_TOKEN permissions for your organization",
            "url": "https://docs.github.com/en/organizations/managing-organization-settings/disabling-or-limiting-github-actions-for-your-organization"
          },
          "scenarioIds": [
            "org-nx-s1ngularity"
          ],
          "meta": {
            "cicdSec": [
              5
            ],
            "plan": "free",
            "lastVerified": "2026-10-11"
          },
          "detect": {
            "events": [
              "org.set_default_workflow_permissions",
              "business.set_default_workflow_permissions",
              "repo.set_default_workflow_permissions"
            ],
            "match": "exact"
          }
        },
        {
          "id": "cicd-no-actions-pr-approval",
          "text": "Turn off \"Allow GitHub Actions to create and approve pull requests\"",
          "tier": "quick-win",
          "why": "With it on, a workflow's approval counts towards required reviews, so anyone who can change a workflow can merge without a person reviewing.",
          "check": {
            "scope": "org",
            "calls": [
              "/orgs/{org}/actions/permissions/workflow"
            ],
            "jq": ".[0].can_approve_pull_request_reviews | not",
            "detail": "\"can_approve_pull_request_reviews: \\(.[0].can_approve_pull_request_reviews)\"",
            "needs": "Organization owner (classic token: admin:org, read:org)",
            "note": "Repositories can still turn it on for themselves unless the organization or enterprise setting is off."
          },
          "prerequisites": [
            "Organization owner",
            "or Enterprise owner to enforce it for all orgs"
          ],
          "steps": [
            "Go to `github.com/organizations/ORG/settings/actions`.",
            "Under \"Workflow permissions\", untick \"Allow GitHub Actions to create and approve pull requests\". Save.",
            "If a bot needs to open pull requests, give it a GitHub App with Pull requests: write instead, and keep its approvals out of required reviews."
          ],
          "docs": {
            "title": "Disabling or limiting GitHub Actions for your organization",
            "url": "https://docs.github.com/en/organizations/managing-organization-settings/disabling-or-limiting-github-actions-for-your-organization"
          },
          "scenarioIds": [
            "org-nx-s1ngularity"
          ],
          "meta": {
            "cicdSec": [
              1,
              5
            ],
            "plan": "free",
            "lastVerified": "2026-10-11"
          },
          "detect": {
            "events": [
              "org.set_workflow_permission_can_approve_pr",
              "business.set_workflow_permission_can_approve_pr",
              "repo.set_workflow_permission_can_approve_pr"
            ],
            "match": "exact"
          }
        },
        {
          "id": "cicd-fork-approval",
          "text": "Require approval for first-time/external contributor workflow runs on forks",
          "tier": "quick-win",
          "why": "An unreviewed fork PR can run arbitrary workflow code the moment it's opened.",
          "check": {
            "scope": "org",
            "calls": [
              "/orgs/{org}/actions/permissions/fork-pr-contributor-approval"
            ],
            "jq": ".[0].approval_policy == \"all_external_contributors\"",
            "detail": "\"approval_policy: \\(.[0].approval_policy)\"",
            "needs": "Organization owner (classic token: admin:org, read:org)"
          },
          "prerequisites": [
            "Organization owner"
          ],
          "steps": [
            "Go to `github.com/organizations/ORG/settings/actions`.",
            "Under \"Approval for running fork pull request workflows from contributors\", select \"Require approval for all external contributors\". Save.",
            "For private repos, under the private fork policy, leave \"Send write tokens\" and \"Send secrets\" off.",
            "Before approving a run, read the diff, especially changes under `.github/`."
          ],
          "docs": {
            "title": "Disabling or limiting GitHub Actions for your organization",
            "url": "https://docs.github.com/en/organizations/managing-organization-settings/disabling-or-limiting-github-actions-for-your-organization"
          },
          "scenarioIds": [
            "org-hackerbot-claw"
          ],
          "meta": {
            "cicdSec": [
              4
            ],
            "plan": "free"
          },
          "detect": {
            "events": [
              "org.set_actions_fork_pr_approvals_policy",
              "org.set_actions_private_fork_pr_approvals_policy",
              "org.set_fork_pr_workflows_policy",
              "repo.set_actions_fork_pr_approvals_policy"
            ],
            "match": "exact"
          }
        },
        {
          "id": "cicd-no-selfhosted-public",
          "text": "Disallow self-hosted runners on public repositories",
          "tier": "quick-win",
          "why": "A public repo's self-hosted runner is reachable by anyone who can open a pull request against it.",
          "check": {
            "scope": "org",
            "calls": [
              "/orgs/{org}/actions/runner-groups?per_page=100"
            ],
            "jq": ".[0].runner_groups | all(.allows_public_repositories | not)",
            "detail": "(.[0].runner_groups | map(select(.allows_public_repositories) | .name)) as $bad | if ($bad | length) == 0 then \"No runner group allows public repositories (\\(.[0].runner_groups | length) groups)\" else \"Runner groups open to public repos: \\($bad | join(\", \"))\" end",
            "needs": "Organization owner (classic token: admin:org, read:org)",
            "note": "Repo-level self-hosted runners are not checked."
          },
          "prerequisites": [
            "Organization owner"
          ],
          "steps": [
            "Go to `github.com/organizations/ORG/settings/actions/runner-groups`.",
            "Open each runner group and untick \"Allow public repositories\". Save.",
            "Point public repos at GitHub-hosted runners instead.",
            "Check each public repo's `github.com/ORG/REPO/settings/actions/runners` for repo-level self-hosted runners and remove them."
          ],
          "docs": {
            "title": "Managing access to self-hosted runners",
            "url": "https://docs.github.com/en/actions/how-tos/manage-runners/self-hosted-runners/manage-access"
          },
          "scenarioIds": [
            "org-self-hosted-runner-attacks"
          ],
          "meta": {
            "cicdSec": [
              4,
              5
            ],
            "plan": "free",
            "lastVerified": "2026-10-11"
          },
          "detect": {
            "events": [
              "org.runner_group_updated",
              "org.runner_group_visiblity_updated",
              "repo.register_self_hosted_runner"
            ],
            "match": "related"
          }
        },
        {
          "id": "cicd-secret-inventory",
          "text": "Inventory every Actions secret and its visibility/scope; the results are usually surprising",
          "tier": "quick-win",
          "why": "You can't scope what you haven't inventoried. Most orgs are surprised by what's exposed org-wide.",
          "check": {
            "scope": "org",
            "calls": [
              "/orgs/{org}/actions/secrets?per_page=100"
            ],
            "jq": ".[0].secrets | all(.visibility != \"all\")",
            "detail": "(.[0].secrets | map(select(.visibility == \"all\") | .name)) as $all | \"\\(.[0].secrets | length) org secrets; shared with all repos: \\(if ($all | length) == 0 then \"none\" else ($all | join(\", \")) end)\"",
            "needs": "Organization owner (classic token: admin:org, read:org)",
            "note": "Passes when no org secret is shared with every repo. Repo and environment secrets are not listed."
          },
          "prerequisites": [
            "Organization owner for org secrets",
            "repo admin for repo and environment secrets"
          ],
          "steps": [
            "Go to `github.com/organizations/ORG/settings/secrets/actions`. For each secret, note its repository access.",
            "Change any secret set to \"All repositories\" to \"Selected repositories\".",
            "Check `github.com/ORG/REPO/settings/secrets/actions` in each repo, or list repo and environment secrets with `gh secret list -R ORG/REPO` and `gh secret list -R ORG/REPO --env ENV`.",
            "Check the Dependabot and Codespaces secrets tabs too.",
            "Record owner and rotation date for each secret. Delete ones nobody claims."
          ],
          "docs": {
            "title": "Using secrets in GitHub Actions",
            "url": "https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/use-secrets"
          },
          "scenarioIds": [
            "org-ghostaction"
          ],
          "meta": {
            "cicdSec": [
              6
            ],
            "plan": "free"
          },
          "detect": {
            "events": [
              "org.create_actions_secret",
              "org.update_actions_secret",
              "repo.create_actions_secret",
              "environment.create_actions_secret"
            ],
            "match": "related"
          }
        },
        {
          "id": "cicd-pin-actions-sha",
          "text": "Third-party Actions pinned to a full commit SHA instead of a mutable tag or branch",
          "tier": "quarter",
          "kind": "coverage",
          "why": "A mutable tag can point to different code tomorrow than it does today, with no warning to you.",
          "check": {
            "scope": "org",
            "calls": [
              "/orgs/{org}/actions/permissions"
            ],
            "jq": "if .[0].sha_pinning_required == true then true else null end",
            "detail": "if .[0].sha_pinning_required == true then \"The org requires actions to be pinned to a full commit SHA\" else \"The org does not enforce SHA pinning; workflow files were not scanned\" end",
            "needs": "Organization owner (classic token: admin:org, read:org)",
            "note": "Only reads the org-wide enforcement setting. Per-workflow pinning is not scanned yet."
          },
          "prerequisites": [
            "Write access to workflows",
            "Organization owner to enforce it"
          ],
          "steps": [
            "Find unpinned actions: `grep -rn \"uses:\" .github/workflows | grep -vE \"@[0-9a-f]{40}\"`.",
            "Replace each tag with its full commit SHA plus a comment, e.g. `actions/checkout@<sha> # v4.2.2`. Tools like pinact or ratchet do this in bulk.",
            "Add a Dependabot config with `package-ecosystem: github-actions` so the pinned SHAs get update PRs.",
            "Once repos are pinned, go to `github.com/organizations/ORG/settings/actions` and turn on \"Require actions to be pinned to a full-length commit SHA\"."
          ],
          "docs": {
            "title": "Secure use reference: using third-party actions",
            "url": "https://docs.github.com/en/actions/reference/security/secure-use"
          },
          "scenarioIds": [
            "app-tj-actions-changed-files",
            "org-teampcp"
          ],
          "meta": {
            "cicdSec": [
              3
            ],
            "plan": "free"
          },
          "detect": {
            "events": [
              "org.update_actions_settings",
              "workflows.actions_policy_violation"
            ],
            "match": "related"
          }
        },
        {
          "id": "cicd-audit-pull-request-target",
          "text": "Audit every use of pull_request_target and remove or restructure risky ones",
          "tier": "quarter",
          "why": "This trigger hands secrets and write access to a workflow that can be made to run untrusted code.",
          "prerequisites": [
            "Read access to the repos",
            "write access to fix them"
          ],
          "steps": [
            "Search: `github.com/search?q=org:ORG+pull_request_target+path:.github/workflows&type=code`.",
            "For each hit, check whether it checks out or runs code from the PR (for example `ref: ${{ github.event.pull_request.head.sha }}`).",
            "If it does, switch to `pull_request`, or split it into a `pull_request` job plus a `workflow_run` job that holds the secrets.",
            "Never put `${{ github.event.* }}` straight into `run:`. Pass it through `env:` instead.",
            "Add a minimal `permissions:` block. Run zizmor or CodeQL for Actions to catch what you missed."
          ],
          "docs": {
            "title": "Secure use reference",
            "url": "https://docs.github.com/en/actions/reference/security/secure-use"
          },
          "scenarioIds": [
            "org-pwn-requests",
            "org-ultralytics"
          ],
          "meta": {
            "cicdSec": [
              4
            ],
            "plan": "free"
          }
        },
        {
          "id": "cicd-verified-actions-only",
          "text": "Restrict which Actions can run to GitHub-verified creators or an internal allow-list",
          "tier": "quarter",
          "why": "An unverified action is arbitrary code with access to your repo and secrets by default.",
          "check": {
            "scope": "org",
            "calls": [
              "/orgs/{org}/actions/permissions"
            ],
            "jq": "if .[0].enabled_repositories == \"none\" then true elif .[0].allowed_actions == null then null else .[0].allowed_actions != \"all\" end",
            "detail": "\"enabled_repositories: \\(.[0].enabled_repositories), allowed_actions: \\(.[0].allowed_actions // \"n/a\")\"",
            "needs": "Organization owner (classic token: admin:org, read:org)"
          },
          "prerequisites": [
            "Organization owner"
          ],
          "steps": [
            "List the actions in use today (search `uses:` across the org) so you know what to allow.",
            "Go to `github.com/organizations/ORG/settings/actions`.",
            "Under \"Policies\", select the option that allows your own org's actions plus selected outside actions and reusable workflows.",
            "Tick \"Allow actions created by GitHub\" and \"Allow actions by Marketplace verified creators\".",
            "Add every other action you need to the allow list as `owner/action@<sha>`. Save."
          ],
          "docs": {
            "title": "Allowing select actions to run",
            "url": "https://docs.github.com/en/organizations/managing-organization-settings/disabling-or-limiting-github-actions-for-your-organization"
          },
          "scenarioIds": [
            "app-tj-actions-changed-files"
          ],
          "meta": {
            "cicdSec": [
              3,
              8
            ],
            "plan": "free"
          },
          "detect": {
            "events": [
              "org.update_actions_settings",
              "business.update_actions_settings",
              "repo.update_actions_settings",
              "workflows.actions_policy_violation"
            ],
            "match": "exact"
          }
        },
        {
          "id": "cicd-rotation-runbook",
          "text": "Write and rehearse a credential-rotation runbook before you need it",
          "tier": "quarter",
          "why": "A rotation plan practiced in advance is the difference between a 30-minute incident and a multi-day one.",
          "prerequisites": [
            "Owners for each credential type (GitHub, cloud, registries)"
          ],
          "steps": [
            "List every credential type: PATs, GitHub App keys, deploy keys, Actions secrets, cloud keys, registry tokens.",
            "For each, write who can rotate it, where it's used, and the exact revoke and replace steps.",
            "Add how to check the audit log for use of the old credential after revoking it.",
            "Store the runbook where on-call can find it during an incident.",
            "Rehearse it once a year by rotating one real credential end to end."
          ],
          "meta": {
            "cicdSec": [
              6
            ],
            "plan": "free"
          }
        },
        {
          "id": "cicd-oidc-federation",
          "text": "Cloud credentials migrated from static keys to OIDC federation, scoped to repo + branch",
          "tier": "initiative",
          "kind": "coverage",
          "why": "A static cloud key that leaks works from anywhere, forever; a scoped OIDC trust policy doesn't.",
          "prerequisites": [
            "IAM admin in the cloud account",
            "write access to the workflow"
          ],
          "steps": [
            "In your cloud, add `token.actions.githubusercontent.com` as an OIDC identity provider.",
            "Create a role whose trust policy restricts `sub` to one repo and branch or environment, e.g. `repo:ORG/REPO:ref:refs/heads/main`.",
            "In the workflow, set `permissions: id-token: write` and use the cloud's login action (e.g. `aws-actions/configure-aws-credentials` with `role-to-assume`).",
            "Delete the old static key from Actions secrets and deactivate it in the cloud."
          ],
          "docs": {
            "title": "OpenID Connect in GitHub Actions",
            "url": "https://docs.github.com/en/actions/concepts/security/openid-connect"
          },
          "scenarioIds": [
            "org-nx-s1ngularity"
          ],
          "meta": {
            "cicdSec": [
              6
            ],
            "plan": "free"
          },
          "detect": {
            "events": [
              "org.create_actions_secret",
              "repo.create_actions_secret",
              "environment.create_actions_secret"
            ],
            "match": "related"
          }
        },
        {
          "id": "cicd-github-apps-over-pats",
          "text": "Service-account automation migrated from PATs to GitHub Apps",
          "tier": "initiative",
          "kind": "coverage",
          "why": "A PAT inherits its creator's full permissions; a GitHub App only ever has what you explicitly grant it.",
          "prerequisites": [
            "Organization owner or GitHub App manager"
          ],
          "steps": [
            "Register an app at `github.com/organizations/ORG/settings/apps/new`. Turn off the webhook and grant only the permissions the job needs.",
            "Generate a private key and store it with the App ID as Actions secrets.",
            "Install the app on only the repositories it needs.",
            "In workflows, mint a short-lived token with `actions/create-github-app-token` and use it where the PAT was.",
            "Revoke the old PAT."
          ],
          "docs": {
            "title": "Authenticating with a GitHub App in a workflow",
            "url": "https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/making-authenticated-api-requests-with-a-github-app-in-a-github-actions-workflow"
          },
          "scenarioIds": [
            "app-installation-token-abuse"
          ],
          "meta": {
            "cicdSec": [
              2,
              6
            ],
            "plan": "free"
          },
          "detect": {
            "events": [
              "personal_access_token.access_granted",
              "personal_access_token.request_created",
              "personal_access_token.access_restriction_disabled"
            ],
            "match": "related"
          }
        },
        {
          "id": "cicd-persist-credentials",
          "text": "Set `persist-credentials: false` on actions/checkout wherever later steps do not push",
          "tier": "quarter",
          "kind": "coverage",
          "why": "By default actions/checkout leaves the job's token on disk for the rest of the job, where any later step, or an uploaded artifact, can pick it up.",
          "prerequisites": [
            "Write access to the workflows"
          ],
          "steps": [
            "Search the organization's workflows for `actions/checkout`.",
            "Add `persist-credentials: false` under `with:` on every checkout whose job does not run authenticated git commands afterwards.",
            "Where a job does need to push, pass the token only to that step.",
            "A workflow linter such as zizmor flags checkouts that persist credentials (see the Tooling page)."
          ],
          "docs": {
            "title": "actions/checkout: persist-credentials",
            "url": "https://github.com/actions/checkout"
          },
          "scenarioIds": [
            "app-artipacked"
          ],
          "meta": {
            "cicdSec": [
              4,
              6
            ],
            "plan": "free",
            "lastVerified": "2026-10-11"
          }
        },
        {
          "id": "cicd-environment-reviewers",
          "text": "Put production secrets in environments with required reviewers",
          "tier": "quarter",
          "kind": "coverage",
          "why": "Anyone with write access can read repository secrets through a workflow. Environment secrets are only given to a job after the environment's rules, such as required reviewers, pass.",
          "prerequisites": [
            "Admin on the repository"
          ],
          "steps": [
            "In each repository that deploys, create a `production` environment under Settings, Environments.",
            "Add required reviewers (a small team, not the people who write the code alone) and restrict which branches can deploy.",
            "Move deployment secrets from repository secrets into the environment, and delete the repository copies.",
            "Reference the environment in the deploy job with `environment: production`."
          ],
          "docs": {
            "title": "Managing environments for deployment",
            "url": "https://docs.github.com/en/actions/how-tos/deploy/configure-and-manage-deployments/manage-environments"
          },
          "scenarioIds": [
            "org-ghostaction",
            "org-chaindrop"
          ],
          "meta": {
            "cicdSec": [
              1,
              5
            ],
            "plan": "enterprise",
            "planNote": "Free and Team: public repositories only.",
            "lastVerified": "2026-10-11"
          },
          "detect": {
            "events": [
              "environment.remove_protection_rule",
              "environment.update_protection_rule",
              "repo.create_actions_secret",
              "workflows.approve_workflow_job"
            ],
            "match": "exact"
          }
        },
        {
          "id": "cicd-runner-groups-selected",
          "text": "Limit each self-hosted runner group to the repositories that need it",
          "tier": "quarter",
          "why": "A runner group open to all repositories lets any repository, including one an attacker can write to, run jobs on those machines and reach what they can reach.",
          "check": {
            "scope": "org",
            "calls": [
              "/orgs/{org}/actions/runner-groups?per_page=100"
            ],
            "jq": ".[0].runner_groups | all(.visibility != \"all\")",
            "detail": "(.[0].runner_groups | map(select(.visibility == \"all\") | .name)) as $open | if ($open | length) == 0 then \"Every runner group is limited (\\(.[0].runner_groups | length) groups)\" else \"Runner groups open to all repositories: \\($open | join(\", \"))\" end",
            "needs": "Organization owner (classic token: admin:org, read:org)",
            "note": "The Default group counts too. If it holds no self-hosted runners, it is safe to accept."
          },
          "prerequisites": [
            "Organization owner"
          ],
          "steps": [
            "Go to `github.com/organizations/ORG/settings/actions/runner-groups`.",
            "For each group, set Repository access to \"Selected repositories\" and pick only the repositories that need those runners.",
            "Keep runners with access to production networks or credentials in their own group, used only by deployment repositories."
          ],
          "docs": {
            "title": "Managing access to self-hosted runners",
            "url": "https://docs.github.com/en/actions/how-tos/manage-runners/self-hosted-runners/manage-access"
          },
          "scenarioIds": [
            "org-self-hosted-runner-attacks"
          ],
          "meta": {
            "cicdSec": [
              5
            ],
            "plan": "team",
            "planNote": "Free organizations have only the default group.",
            "lastVerified": "2026-10-11"
          },
          "detect": {
            "events": [
              "org.runner_group_updated",
              "org.runner_group_visiblity_updated",
              "enterprise.runner_group_updated"
            ],
            "match": "exact"
          }
        },
        {
          "id": "cicd-release-cache-isolation",
          "text": "Do not restore Actions caches in release and publish workflows",
          "tier": "quarter",
          "why": "A cache written by a less trusted workflow can be restored by a more trusted one. A poisoned cache in a release build ships the attacker's code under your name.",
          "prerequisites": [
            "Write access to the workflows"
          ],
          "steps": [
            "List the workflows that build releases, publish packages or deploy.",
            "Remove `actions/cache` steps and the `cache:` option of setup actions (setup-node, setup-python and others) from those workflows.",
            "Keep caching in pull request and test workflows, where it saves time and a poisoned cache only affects a test run.",
            "zizmor's cache-poisoning audit finds release workflows that still restore caches."
          ],
          "docs": {
            "title": "Dependency caching reference",
            "url": "https://docs.github.com/en/actions/reference/workflows-and-actions/dependency-caching"
          },
          "scenarioIds": [
            "org-cache-poisoning"
          ],
          "meta": {
            "cicdSec": [
              4,
              9
            ],
            "plan": "free",
            "lastVerified": "2026-10-11"
          }
        },
        {
          "id": "cicd-trusted-publishing",
          "text": "Publish packages with trusted publishing (OIDC) instead of long-lived registry tokens",
          "tier": "quarter",
          "kind": "coverage",
          "why": "A stored npm or PyPI token can be stolen and used from anywhere. Trusted publishing gives a short-lived credential to one named workflow instead.",
          "prerequisites": [
            "Owner of the package on the registry",
            "Write access to the publishing workflow"
          ],
          "steps": [
            "On the registry (npm, PyPI and others), add a trusted publisher for the package: the repository and the publishing workflow file, and an environment if you use one.",
            "Give the publishing job `permissions: id-token: write` and remove the registry token from the workflow.",
            "Revoke the old registry token, and turn off token-based publishing on the registry where it allows that."
          ],
          "docs": {
            "title": "npm: Trusted publishing",
            "url": "https://docs.npmjs.com/trusted-publishers"
          },
          "scenarioIds": [
            "org-shai-hulud",
            "org-nx-s1ngularity"
          ],
          "meta": {
            "cicdSec": [
              6,
              9
            ],
            "plan": "free",
            "lastVerified": "2026-10-11"
          },
          "detect": {
            "events": [
              "org.create_actions_secret",
              "repo.create_actions_secret",
              "environment.create_actions_secret"
            ],
            "match": "related"
          }
        }
      ]
    }
  ],
  "ghasItems": [
    {
      "id": "ghas-secret-scanning",
      "text": "Secret scanning on private/internal repos",
      "license": "secret-protection",
      "description": "Detects known secret patterns (cloud keys, tokens, credentials) committed anywhere in private or internal repositories.",
      "url": "https://docs.github.com/en/code-security/concepts/secret-security/secret-scanning",
      "why": "Would have caught the harvested secrets in GhostAction before they ever left the repo.",
      "prerequisites": [
        "Organization owner or Security manager"
      ],
      "steps": [
        "Go to `github.com/organizations/ORG/settings/security_products`.",
        "Create a configuration with Secret Protection enabled, or use the GitHub recommended one.",
        "Apply it to your private and internal repositories and set it as the default for new ones.",
        "Review open alerts at `github.com/orgs/ORG/security` and rotate anything valid."
      ],
      "scenarioIds": [
        "org-ghostaction"
      ],
      "meta": {
        "cicdSec": [
          6
        ],
        "plan": "team"
      },
      "detect": {
        "events": [
          "repository_secret_scanning.disable",
          "secret_scanning.disable",
          "secret_scanning_new_repos.disable",
          "repository_security_configuration.removed"
        ],
        "match": "exact"
      }
    },
    {
      "id": "ghas-push-protection",
      "text": "Push protection on private/internal repos",
      "license": "secret-protection",
      "description": "Blocks a git push containing a detected secret before it ever reaches repository history, instead of just alerting after the fact.",
      "url": "https://docs.github.com/en/code-security/concepts/secret-security/push-protection",
      "why": "Stops a secret at the push itself, rather than relying on scanning and rotation after the fact.",
      "prerequisites": [
        "Organization owner or Security manager"
      ],
      "steps": [
        "Go to `github.com/organizations/ORG/settings/security_products` and edit your configuration.",
        "Turn on Push protection. Apply it to private and internal repositories.",
        "Optionally turn on delegated bypass so a push with a secret needs a reviewer's approval to go through."
      ],
      "scenarioIds": [
        "org-ghostaction"
      ],
      "meta": {
        "cicdSec": [
          6
        ],
        "plan": "team"
      },
      "detect": {
        "events": [
          "repository_secret_scanning_push_protection.disable",
          "org.secret_scanning_push_protection_disable",
          "secret_scanning_push_protection.bypass",
          "org_secret_scanning_push_protection_bypass_list.add"
        ],
        "match": "exact"
      }
    },
    {
      "id": "ghas-code-scanning",
      "text": "Code scanning (CodeQL) on private/internal repos",
      "license": "code-security",
      "description": "Runs static analysis against every push and pull request to catch injection flaws, unsafe deserialization, and other code-level vulnerabilities before merge.",
      "url": "https://docs.github.com/en/code-security/concepts/code-scanning/code-scanning",
      "why": "Catches the injection-class bugs that let attacker-controlled input reach a shell in the first place.",
      "prerequisites": [
        "Organization owner or Security manager"
      ],
      "steps": [
        "Go to `github.com/organizations/ORG/settings/security_products`.",
        "In your configuration, enable Code Security with code scanning default setup.",
        "Apply it to private and internal repositories.",
        "Add \"Require code scanning results\" to your branch ruleset so high-severity findings block merges."
      ],
      "meta": {
        "cicdSec": [
          4,
          7
        ],
        "plan": "team"
      },
      "detect": {
        "events": [
          "org.codeql_disabled",
          "repo.codeql_disabled",
          "repository_security_configuration.removed",
          "code_scanning.alert_closed_by_user"
        ],
        "match": "exact"
      }
    },
    {
      "id": "ghas-dependency-review",
      "text": "Dependency review on pull requests",
      "license": "code-security",
      "description": "Surfaces vulnerable or license-risky dependency changes directly in the PR diff, before a risky version ever gets merged in.",
      "url": "https://docs.github.com/en/code-security/concepts/supply-chain-security/dependency-review",
      "why": "Flags a malicious or vulnerable dependency bump directly in the PR diff, before it merges.",
      "prerequisites": [
        "write access to workflows"
      ],
      "steps": [
        "Make sure the dependency graph is on in your security configuration at `github.com/organizations/ORG/settings/security_products`.",
        "Add a workflow on `pull_request` that runs `actions/dependency-review-action` (pinned to a SHA).",
        "Set `fail-on-severity: high` (or lower) in its config.",
        "Add the check as a required status check in your branch ruleset."
      ],
      "scenarioIds": [
        "app-tj-actions-changed-files"
      ],
      "meta": {
        "cicdSec": [
          3
        ],
        "plan": "team",
        "planNote": "Free on public repositories."
      },
      "detect": {
        "events": [
          "dependency_graph.disable",
          "repository_dependency_graph.disable",
          "required_status_check.destroy",
          "repository_ruleset.update"
        ],
        "match": "related"
      }
    },
    {
      "id": "ghas-security-overview",
      "text": "Security overview & security campaigns",
      "license": "code-security",
      "description": "An org-wide dashboard aggregating alerts across every repository, with the ability to launch a prioritized, assignable remediation campaign against them.",
      "url": "https://docs.github.com/en/code-security/concepts/security-at-scale/security-overview",
      "why": "One dashboard beats manually checking alerts repo-by-repo across an entire organization.",
      "prerequisites": [
        "Organization owner or Security manager"
      ],
      "steps": [
        "Go to `github.com/orgs/ORG/security` to see alerts and coverage across all repos.",
        "Filter to the repos or alert types that matter most, for example critical code scanning alerts.",
        "Create a campaign from that filter under `github.com/orgs/ORG/security/campaigns`, with a due date and a contact."
      ],
      "meta": {
        "cicdSec": [
          10
        ],
        "plan": "team",
        "planNote": "Code Security is needed for security campaigns."
      },
      "detect": {
        "events": [
          "org.advanced_security_disabled_on_all_repos",
          "repository_security_configuration.removed"
        ],
        "match": "related"
      }
    },
    {
      "id": "ghas-copilot-autofix",
      "text": "Copilot Autofix for code scanning alerts",
      "license": "code-security",
      "description": "Generates an AI-suggested, one-click fix for a CodeQL finding directly on the pull request, cutting remediation time on the alerts you do have.",
      "why": "Lowers the effort of fixing a finding enough that alerts actually get resolved instead of ignored.",
      "prerequisites": [
        "Code scanning enabled"
      ],
      "steps": [
        "Go to `github.com/organizations/ORG/settings/security_products` and check Copilot Autofix is on in your configuration.",
        "Open a PR with a code scanning alert and review the suggested fix before committing it."
      ],
      "meta": {
        "cicdSec": [
          4
        ],
        "plan": "team"
      },
      "detect": {
        "events": [
          "org.code_scanning_autofix_disabled",
          "repo.code_scanning_autofix_disabled",
          "business.code_scanning_autofix_policy_update"
        ],
        "match": "exact"
      }
    }
  ]
}
